fix: criação do footer totalizador na tabela analitica

This commit is contained in:
Alessandro Gonçaalves 2025-10-20 18:57:27 -03:00
parent e160f66eb3
commit d2c468dddb
1 changed files with 204 additions and 120 deletions

View File

@ -154,10 +154,10 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
header: "Código da Conta", header: "Código da Conta",
filterFn: "advancedText", filterFn: "advancedText",
}, },
{ {
accessorKey: "conta", accessorKey: "conta",
header: "Nome da Conta", header: "Nome da Conta",
filterFn: "advancedText" filterFn: "advancedText",
}, },
{ {
accessorKey: "valor", accessorKey: "valor",
@ -205,9 +205,9 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
header: "Histórico 2", header: "Histórico 2",
filterFn: "advancedText", filterFn: "advancedText",
}, },
{ {
accessorKey: "numero_lancamento", accessorKey: "numero_lancamento",
header: "Número do Lançamento", header: "Número do Lançamento",
filterFn: "advancedText", filterFn: "advancedText",
cell: () => "-", cell: () => "-",
}, },
@ -312,32 +312,50 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
React.useEffect(() => { React.useEffect(() => {
// Usar dados filtrados da tabela em vez dos dados originais // Usar dados filtrados da tabela em vez dos dados originais
const filteredData = table.getRowModel().rows.map(row => row.original); const filteredData = table.getRowModel().rows.map((row) => row.original);
const newTotal = filteredData.reduce((sum, item) => { const newTotal = filteredData.reduce((sum, item) => {
const valor = const valor =
typeof item.valor === "string" ? parseFloat(item.valor) : item.valor; typeof item.valor === "string" ? parseFloat(item.valor) : item.valor;
return sum + (isNaN(valor) ? 0 : valor); return sum + (isNaN(valor) ? 0 : valor);
}, 0); }, 0);
console.log('🔄 Calculando total:', { console.log("🔄 Calculando total:", {
totalRows: table.getRowModel().rows.length, totalRows: table.getRowModel().rows.length,
originalDataLength: data.length, originalDataLength: data.length,
newTotal, newTotal,
columnFilters: columnFilters.length, columnFilters: columnFilters.length,
globalFilter globalFilter,
}); });
setTotalValor(newTotal); setTotalValor(newTotal);
}, [table, data, columnFilters, globalFilter]); }, [table, data, columnFilters, globalFilter]);
// Calcular totais das colunas de valores para o footer - mesmo princípio do Valor Total
const columnTotals = React.useMemo(() => {
// Usar dados filtrados da tabela em vez dos dados originais - MESMA LÓGICA DO totalValor
const filteredData = table.getRowModel().rows.map((row) => row.original);
const valorRealizado = filteredData.reduce((sum, item) => {
const valor =
typeof item.valor === "string" ? parseFloat(item.valor) : item.valor;
return sum + (isNaN(valor) ? 0 : valor);
}, 0);
return {
valorRealizado,
valorPrevisto: 0, // Sempre 0 pois não há dados
valorConfirmado: 0, // Sempre 0 pois não há dados
valorPago: 0, // Sempre 0 pois não há dados
};
}, [table]);
const exportToExcel = () => { const exportToExcel = () => {
if (data.length === 0) return; if (data.length === 0) return;
// Usar dados filtrados da tabela em vez dos dados originais // Usar dados filtrados da tabela em vez dos dados originais
const filteredData = table.getRowModel().rows.map(row => row.original); const filteredData = table.getRowModel().rows.map((row) => row.original);
if (filteredData.length === 0) { if (filteredData.length === 0) {
alert('Nenhum dado filtrado para exportar'); alert("Nenhum dado filtrado para exportar");
return; return;
} }
@ -371,7 +389,10 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
const resumoData = [ const resumoData = [
{ Métrica: "Total de Registros", Valor: filteredData.length }, { Métrica: "Total de Registros", Valor: filteredData.length },
{ Métrica: "Valor Total", Valor: totalValor }, { Métrica: "Valor Total", Valor: totalValor },
{ Métrica: "Filtros Aplicados", Valor: columnFilters.length > 0 || globalFilter ? "Sim" : "Não" }, {
Métrica: "Filtros Aplicados",
Valor: columnFilters.length > 0 || globalFilter ? "Sim" : "Não",
},
]; ];
const wsResumo = XLSX.utils.json_to_sheet(resumoData); const wsResumo = XLSX.utils.json_to_sheet(resumoData);
@ -383,7 +404,9 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
const now = new Date(); const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, "-"); const timestamp = now.toISOString().slice(0, 19).replace(/:/g, "-");
const hasFilters = columnFilters.length > 0 || globalFilter; const hasFilters = columnFilters.length > 0 || globalFilter;
const fileName = `analitico${hasFilters ? '_filtrado' : ''}_${timestamp}.xlsx`; const fileName = `analitico${
hasFilters ? "_filtrado" : ""
}_${timestamp}.xlsx`;
// Fazer download // Fazer download
XLSX.writeFile(wb, fileName); XLSX.writeFile(wb, fileName);
@ -452,17 +475,17 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
Limpar Filtros Limpar Filtros
</Button> </Button>
)} )}
{data.length > 0 && ( {data.length > 0 && (
<Button <Button
onClick={exportToExcel} onClick={exportToExcel}
variant="outline" variant="outline"
size="sm" size="sm"
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-green-50 hover:border-green-300 text-gray-700" className="flex items-center gap-2 bg-white border-gray-300 hover:bg-green-50 hover:border-green-300 text-gray-700"
> >
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
Exportar XLSX Exportar XLSX
</Button> </Button>
)} )}
</div> </div>
</div> </div>
@ -472,23 +495,39 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
{/* Table Header */} {/* Table Header */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200 sticky top-0 z-20"> <div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200 sticky top-0 z-20">
<div className="flex items-center px-4 py-3 text-xs font-semibold text-gray-700 uppercase tracking-wide"> <div className="flex items-center px-4 py-3 text-xs font-semibold text-gray-700 uppercase tracking-wide">
<div className="w-[140px] whitespace-nowrap">Data de Vencimento</div> <div className="w-[140px] whitespace-nowrap">
Data de Vencimento
</div>
<div className="w-[120px] whitespace-nowrap">Data de Caixa</div> <div className="w-[120px] whitespace-nowrap">Data de Caixa</div>
<div className="w-[100px] whitespace-nowrap">Entidade</div> <div className="w-[100px] whitespace-nowrap">Entidade</div>
<div className="w-[160px] whitespace-nowrap">Código do Fornecedor</div> <div className="w-[160px] whitespace-nowrap">
<div className="w-[220px] whitespace-nowrap">Nome do Fornecedor</div> Código do Fornecedor
</div>
<div className="w-[220px] whitespace-nowrap">
Nome do Fornecedor
</div>
<div className="w-[140px] whitespace-nowrap">Centro de Custo</div> <div className="w-[140px] whitespace-nowrap">Centro de Custo</div>
<div className="w-[130px] whitespace-nowrap">Código da Conta</div> <div className="w-[130px] whitespace-nowrap">Código da Conta</div>
<div className="w-[160px] whitespace-nowrap">Nome da Conta</div> <div className="w-[160px] whitespace-nowrap">Nome da Conta</div>
<div className="w-[130px] whitespace-nowrap text-right">Valor Realizado</div> <div className="w-[130px] whitespace-nowrap text-right">
<div className="w-[120px] whitespace-nowrap text-right">Valor Previsto</div> Valor Realizado
<div className="w-[130px] whitespace-nowrap text-right">Valor Confirmado</div> </div>
<div className="w-[110px] whitespace-nowrap text-right">Valor Pago</div> <div className="w-[120px] whitespace-nowrap text-right">
Valor Previsto
</div>
<div className="w-[130px] whitespace-nowrap text-right">
Valor Confirmado
</div>
<div className="w-[110px] whitespace-nowrap text-right">
Valor Pago
</div>
<div className="w-[200px] whitespace-nowrap">Histórico</div> <div className="w-[200px] whitespace-nowrap">Histórico</div>
<div className="w-[200px] whitespace-nowrap">Histórico 2</div> <div className="w-[200px] whitespace-nowrap">Histórico 2</div>
<div className="w-[50px] whitespace-nowrap">Número do Lançamento</div> <div className="w-[50px] whitespace-nowrap">
Número do Lançamento
</div>
</div>
</div> </div>
</div>
{/* Table Body */} {/* Table Body */}
<div <div
@ -504,8 +543,8 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
<div className="text-center"> <div className="text-center">
<div className="w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-2"></div> <div className="w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-2"></div>
<p className="text-gray-500">Carregando dados...</p> <p className="text-gray-500">Carregando dados...</p>
</div> </div>
</div> </div>
) : virtualRows.length === 0 ? ( ) : virtualRows.length === 0 ? (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="text-center"> <div className="text-center">
@ -523,10 +562,10 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/> />
</svg> </svg>
</div> </div>
<p className="text-gray-500">Nenhum dado encontrado</p> <p className="text-gray-500">Nenhum dado encontrado</p>
</div> </div>
</div> </div>
) : ( ) : (
<div <div
className="relative" className="relative"
@ -544,34 +583,36 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
{new Date( {new Date(
row.original.data_vencimento row.original.data_vencimento
).toLocaleDateString("pt-BR")} ).toLocaleDateString("pt-BR")}
</div> </div>
<div className="w-[120px] text-gray-600 whitespace-nowrap"> <div className="w-[120px] text-gray-600 whitespace-nowrap">
{new Date(row.original.data_caixa).toLocaleDateString( {new Date(row.original.data_caixa).toLocaleDateString(
"pt-BR" "pt-BR"
)} )}
</div> </div>
<div className="w-[100px] text-gray-500 whitespace-nowrap">-</div> <div className="w-[100px] text-gray-500 whitespace-nowrap">
-
</div>
<div className="w-[160px] font-medium text-gray-900 whitespace-nowrap"> <div className="w-[160px] font-medium text-gray-900 whitespace-nowrap">
{row.original.codigo_fornecedor} {row.original.codigo_fornecedor}
</div> </div>
<div <div
className="w-[220px] text-gray-700 truncate" className="w-[220px] text-gray-700 truncate"
title={row.original.nome_fornecedor} title={row.original.nome_fornecedor}
> >
{row.original.nome_fornecedor} {row.original.nome_fornecedor}
</div> </div>
<div className="w-[140px] text-gray-600 whitespace-nowrap"> <div className="w-[140px] text-gray-600 whitespace-nowrap">
{row.original.codigo_centrocusto} {row.original.codigo_centrocusto}
</div> </div>
<div className="w-[130px] text-gray-600 whitespace-nowrap"> <div className="w-[130px] text-gray-600 whitespace-nowrap">
{row.original.codigo_conta} {row.original.codigo_conta}
</div> </div>
<div <div
className="w-[160px] text-gray-700 truncate" className="w-[160px] text-gray-700 truncate"
title={row.original.conta} title={row.original.conta}
> >
{row.original.conta} {row.original.conta}
</div> </div>
<div <div
className={`w-[130px] text-right font-semibold whitespace-nowrap ${ className={`w-[130px] text-right font-semibold whitespace-nowrap ${
row.original.valor < 0 row.original.valor < 0
@ -583,77 +624,120 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
style: "currency", style: "currency",
currency: "BRL", currency: "BRL",
}).format(row.original.valor)} }).format(row.original.valor)}
</div> </div>
<div className="w-[120px] text-gray-500 text-right whitespace-nowrap">-</div> <div className="w-[120px] text-gray-500 text-right whitespace-nowrap">
<div className="w-[130px] text-gray-500 text-right whitespace-nowrap">-</div> -
<div className="w-[110px] text-gray-500 text-right whitespace-nowrap">-</div> </div>
<div className="w-[200px] text-gray-700 truncate" title={row.original.historico}> <div className="w-[130px] text-gray-500 text-right whitespace-nowrap">
-
</div>
<div className="w-[110px] text-gray-500 text-right whitespace-nowrap">
-
</div>
<div
className="w-[200px] text-gray-700 truncate"
title={row.original.historico}
>
{row.original.historico} {row.original.historico}
</div> </div>
<div className="w-[200px] text-gray-700 truncate" title={row.original.historico2}> <div
className="w-[200px] text-gray-700 truncate"
title={row.original.historico2}
>
{row.original.historico2} {row.original.historico2}
</div> </div>
<div className="w-[50px] text-gray-500 whitespace-nowrap">-</div> <div className="w-[50px] text-gray-500 whitespace-nowrap">
</div> -
</div>
</div>
); );
})} })}
</div> </div>
)} )}
</div>
{/* Footer com Totalizador das Colunas */}
{data.length > 0 && (
<div className="bg-gradient-to-r from-gray-50 to-gray-100 border-t border-gray-200 sticky bottom-0 z-10">
<div className="flex items-center px-4 py-3 text-sm font-semibold text-gray-900">
<div className="w-[140px] whitespace-nowrap text-gray-600">
TOTAL: {table.getRowModel().rows.length} registros
</div> </div>
<div className="w-[120px] whitespace-nowrap"></div>
<div className="w-[100px] whitespace-nowrap"></div>
<div className="w-[160px] whitespace-nowrap"></div>
<div className="w-[220px] whitespace-nowrap"></div>
<div className="w-[140px] whitespace-nowrap"></div>
<div className="w-[130px] whitespace-nowrap"></div>
<div className="w-[160px] whitespace-nowrap"></div>
<div
className={`w-[130px] text-right font-bold whitespace-nowrap ${
columnTotals.valorRealizado < 0
? "text-red-600"
: "text-green-600"
}`}
>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valorRealizado)}
</div>
<div className="w-[120px] text-right whitespace-nowrap text-gray-500">
-
</div>
<div className="w-[130px] text-right whitespace-nowrap text-gray-500">
-
</div>
<div className="w-[110px] text-right whitespace-nowrap text-gray-500">
-
</div>
<div className="w-[200px] whitespace-nowrap"></div>
<div className="w-[200px] whitespace-nowrap"></div>
<div className="w-[50px] whitespace-nowrap"></div>
</div>
</div>
)}
{/* Summary Footer - Integrado */} {/* Summary Footer - Integrado */}
{data.length > 0 && ( {
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-t border-blue-200 p-6"> // data.length > 0 && (
<div className="flex justify-between items-center"> // <div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-t border-blue-200 p-6">
<div className="flex items-center gap-4"> // <div className="flex justify-between items-center">
{/* <div className="w-12 h-12 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center"> // <div className="flex items-center gap-4">
<svg // <div>
className="w-6 h-6 text-white" // <h3 className="text-lg font-bold text-gray-900">
fill="none" // Total de Registros:{" "}
stroke="currentColor" // <span className="text-blue-600">
viewBox="0 0 24 24" // {table.getRowModel().rows.length}
> // </span>
<path // </h3>
strokeLinecap="round" // <p className="text-sm text-gray-600">
strokeLinejoin="round" // Transações encontradas
strokeWidth={2} // </p>
d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" // </div>
/> // </div>
</svg> // <div className="text-right">
</div> */} // <h3 className="text-lg font-bold">
<div> // <span
<h3 className="text-lg font-bold text-gray-900"> // className={
Total de Registros:{" "} // totalValor < 0 ? "text-red-600" : "text-green-600"
<span className="text-blue-600"> // }
{table.getRowModel().rows.length} // >
</span> // Valor Total:{" "}
</h3> // {new Intl.NumberFormat("pt-BR", {
<p className="text-sm text-gray-600"> // style: "currency",
Transações encontradas // currency: "BRL",
</p> // }).format(totalValor)}
</div> // </span>
</div> // </h3>
<div className="text-right"> // <p className="text-sm text-gray-600">
<h3 className="text-lg font-bold"> // Soma de todos os valores
<span // </p>
className={ // </div>
totalValor < 0 ? "text-red-600" : "text-green-600" // </div>
} // </div>
> // )
Valor Total:{" "} }
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(totalValor)}
</span>
</h3>
<p className="text-sm text-gray-600">
Soma de todos os valores
</p>
</div>
</div>
</div>
)}
</div> </div>
</div> </div>
@ -698,7 +782,7 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
@ -726,7 +810,7 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
<SelectItem value="notEmpty">não está vazio</SelectItem> <SelectItem value="notEmpty">não está vazio</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{!( {!(
cond.operator === "empty" || cond.operator === "notEmpty" cond.operator === "empty" || cond.operator === "notEmpty"
@ -745,7 +829,7 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
placeholder="Digite o valor" placeholder="Digite o valor"
className="w-full bg-white border-gray-300" className="w-full bg-white border-gray-300"
/> />
</div> </div>
)} )}
{conditions.length > 1 && ( {conditions.length > 1 && (
@ -761,8 +845,8 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
> >
</Button> </Button>
</div> </div>
)} )}
</div> </div>
))} ))}