2025-10-20 20:35:24 +00:00
|
|
|
"use client";
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-20 15:15:53 +00:00
|
|
|
import * as React from "react";
|
|
|
|
|
import {
|
|
|
|
|
useReactTable,
|
|
|
|
|
getCoreRowModel,
|
|
|
|
|
getSortedRowModel,
|
|
|
|
|
getFilteredRowModel,
|
2025-10-21 13:08:00 +00:00
|
|
|
ColumnFiltersState,
|
2025-10-20 15:15:53 +00:00
|
|
|
} from "@tanstack/react-table";
|
|
|
|
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
} from "@/components/ui/dialog";
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
} from "@/components/ui/select";
|
2025-10-20 20:26:14 +00:00
|
|
|
import { Download, Filter, X } from "lucide-react";
|
2025-10-20 20:35:24 +00:00
|
|
|
import * as XLSX from "xlsx";
|
2025-10-08 05:08:35 +00:00
|
|
|
|
|
|
|
|
interface AnaliticoItem {
|
|
|
|
|
codigo_grupo: string;
|
|
|
|
|
codigo_subgrupo: string;
|
|
|
|
|
codigo_fornecedor: string;
|
|
|
|
|
nome_fornecedor: string;
|
|
|
|
|
id: number;
|
|
|
|
|
codfilial: string;
|
|
|
|
|
recnum: number;
|
|
|
|
|
data_competencia: string;
|
|
|
|
|
data_vencimento: string;
|
|
|
|
|
data_pagamento: string;
|
|
|
|
|
data_caixa: string;
|
|
|
|
|
codigo_conta: string;
|
|
|
|
|
conta: string;
|
|
|
|
|
codigo_centrocusto: string;
|
|
|
|
|
valor: number;
|
|
|
|
|
historico: string;
|
|
|
|
|
historico2: string;
|
|
|
|
|
created_at: string;
|
|
|
|
|
updated_at: string;
|
2025-10-21 13:08:00 +00:00
|
|
|
// Campos adicionais do Oracle
|
|
|
|
|
entidade?: string;
|
|
|
|
|
tipo_parceiro?: string;
|
|
|
|
|
valor_previsto?: number;
|
|
|
|
|
valor_confirmado?: number;
|
|
|
|
|
valor_pago?: number;
|
|
|
|
|
numero_lancamento?: number;
|
|
|
|
|
ano_mes_comp?: string;
|
|
|
|
|
codgrupo?: string;
|
2025-10-08 05:08:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface AnaliticoProps {
|
|
|
|
|
filtros: {
|
|
|
|
|
dataInicio: string;
|
|
|
|
|
dataFim: string;
|
|
|
|
|
centroCusto?: string;
|
|
|
|
|
codigoGrupo?: string;
|
|
|
|
|
codigoSubgrupo?: string;
|
|
|
|
|
codigoConta?: string;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
2025-10-20 15:15:53 +00:00
|
|
|
const [data, setData] = React.useState<AnaliticoItem[]>([]);
|
|
|
|
|
const [loading, setLoading] = React.useState(false);
|
|
|
|
|
const [globalFilter, setGlobalFilter] = React.useState("");
|
2025-10-21 13:08:00 +00:00
|
|
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
2025-10-20 15:15:53 +00:00
|
|
|
const [open, setOpen] = React.useState(false);
|
2025-10-20 20:35:24 +00:00
|
|
|
const [conditions, setConditions] = React.useState([
|
|
|
|
|
{ column: "", operator: "contains", value: "" },
|
|
|
|
|
]);
|
2025-10-21 13:08:00 +00:00
|
|
|
|
|
|
|
|
// Estado para armazenar filtros externos (vindos do teste.tsx)
|
|
|
|
|
const [filtrosExternos, setFiltrosExternos] = React.useState(filtros);
|
|
|
|
|
|
|
|
|
|
// Atualizar filtros externos quando os props mudarem, mas preservar filtros internos
|
|
|
|
|
React.useEffect(() => {
|
2025-10-21 13:16:50 +00:00
|
|
|
console.log('🔄 Analítico - useEffect dos filtros chamado');
|
|
|
|
|
console.log('📋 Filtros recebidos via props:', filtros);
|
|
|
|
|
console.log('📋 Filtros externos atuais:', filtrosExternos);
|
2025-10-21 13:08:00 +00:00
|
|
|
setFiltrosExternos(filtros);
|
2025-10-21 13:23:58 +00:00
|
|
|
}, [filtros, filtrosExternos]);
|
2025-10-20 15:15:53 +00:00
|
|
|
|
|
|
|
|
const fetchData = React.useCallback(async () => {
|
2025-10-21 13:16:50 +00:00
|
|
|
console.log('🔄 Analítico - fetchData chamado');
|
|
|
|
|
console.log('📋 Filtros externos recebidos:', filtrosExternos);
|
|
|
|
|
|
2025-10-21 13:08:00 +00:00
|
|
|
// Só faz a requisição se tiver dataInicio e dataFim nos filtros externos
|
|
|
|
|
if (!filtrosExternos.dataInicio || !filtrosExternos.dataFim) {
|
2025-10-21 13:16:50 +00:00
|
|
|
console.log('⚠️ Sem dataInicio ou dataFim, limpando dados');
|
2025-10-08 05:08:35 +00:00
|
|
|
setData([]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
2025-10-21 13:16:50 +00:00
|
|
|
// Construir URL com parâmetros de query
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
|
|
|
|
|
if (filtrosExternos.dataInicio) {
|
|
|
|
|
params.append('dataInicio', filtrosExternos.dataInicio);
|
|
|
|
|
}
|
|
|
|
|
if (filtrosExternos.dataFim) {
|
|
|
|
|
params.append('dataFim', filtrosExternos.dataFim);
|
|
|
|
|
}
|
|
|
|
|
if (filtrosExternos.centroCusto) {
|
|
|
|
|
params.append('centroCusto', filtrosExternos.centroCusto);
|
|
|
|
|
}
|
|
|
|
|
if (filtrosExternos.codigoGrupo) {
|
|
|
|
|
params.append('codigoGrupo', filtrosExternos.codigoGrupo);
|
|
|
|
|
}
|
|
|
|
|
if (filtrosExternos.codigoConta) {
|
|
|
|
|
params.append('codigoConta', filtrosExternos.codigoConta);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const url = `/api/analitico-oracle?${params.toString()}`;
|
|
|
|
|
console.log('🌐 Fazendo requisição para:', url);
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url);
|
2025-10-08 05:08:35 +00:00
|
|
|
if (response.ok) {
|
|
|
|
|
const result = await response.json();
|
2025-10-21 13:16:50 +00:00
|
|
|
console.log('✅ Resposta da API recebida:', result.length, 'registros');
|
|
|
|
|
console.log('📝 Primeiros 3 registros:', result.slice(0, 3));
|
2025-10-21 13:08:00 +00:00
|
|
|
|
2025-10-21 13:16:50 +00:00
|
|
|
setData(result as AnaliticoItem[]);
|
2025-10-08 05:08:35 +00:00
|
|
|
} else {
|
2025-10-21 13:16:50 +00:00
|
|
|
console.error("❌ Erro ao buscar dados:", await response.text());
|
2025-10-08 05:08:35 +00:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-10-21 13:16:50 +00:00
|
|
|
console.error("❌ Erro ao buscar dados:", error);
|
2025-10-08 05:08:35 +00:00
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
2025-10-21 13:08:00 +00:00
|
|
|
}, [filtrosExternos]);
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-20 15:15:53 +00:00
|
|
|
React.useEffect(() => {
|
2025-10-08 05:08:35 +00:00
|
|
|
fetchData();
|
|
|
|
|
}, [fetchData]);
|
|
|
|
|
|
2025-10-20 15:15:53 +00:00
|
|
|
const columns = React.useMemo(
|
|
|
|
|
() => [
|
2025-10-20 20:35:24 +00:00
|
|
|
{
|
2025-10-20 21:45:09 +00:00
|
|
|
accessorKey: "data_vencimento",
|
|
|
|
|
header: "Data de Vencimento",
|
2025-10-20 16:03:14 +00:00
|
|
|
filterFn: "advancedText",
|
|
|
|
|
cell: ({ getValue }: { getValue: () => string }) => {
|
2025-10-20 15:15:53 +00:00
|
|
|
const value = getValue();
|
2025-10-20 20:35:24 +00:00
|
|
|
return new Date(value).toLocaleDateString("pt-BR");
|
|
|
|
|
},
|
2025-10-20 15:15:53 +00:00
|
|
|
},
|
2025-10-20 20:35:24 +00:00
|
|
|
{
|
2025-10-20 21:45:09 +00:00
|
|
|
accessorKey: "data_caixa",
|
|
|
|
|
header: "Data de Caixa",
|
2025-10-20 16:03:14 +00:00
|
|
|
filterFn: "advancedText",
|
|
|
|
|
cell: ({ getValue }: { getValue: () => string }) => {
|
2025-10-20 15:15:53 +00:00
|
|
|
const value = getValue();
|
2025-10-20 20:35:24 +00:00
|
|
|
return new Date(value).toLocaleDateString("pt-BR");
|
|
|
|
|
},
|
2025-10-20 15:15:53 +00:00
|
|
|
},
|
2025-10-20 20:35:24 +00:00
|
|
|
{
|
2025-10-20 21:45:09 +00:00
|
|
|
accessorKey: "entidade",
|
|
|
|
|
header: "Entidade",
|
2025-10-20 16:03:14 +00:00
|
|
|
filterFn: "advancedText",
|
2025-10-21 13:08:00 +00:00
|
|
|
cell: ({ getValue }: { getValue: () => string }) => {
|
|
|
|
|
const value = getValue();
|
|
|
|
|
return value || "-";
|
|
|
|
|
},
|
2025-10-20 20:35:24 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
accessorKey: "codigo_fornecedor",
|
2025-10-20 21:45:09 +00:00
|
|
|
header: "Código do Fornecedor",
|
2025-10-20 20:35:24 +00:00
|
|
|
filterFn: "advancedText",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
accessorKey: "nome_fornecedor",
|
2025-10-20 21:45:09 +00:00
|
|
|
header: "Nome do Fornecedor",
|
2025-10-20 20:35:24 +00:00
|
|
|
filterFn: "advancedText",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
accessorKey: "codigo_centrocusto",
|
2025-10-20 21:45:09 +00:00
|
|
|
header: "Centro de Custo",
|
2025-10-20 20:35:24 +00:00
|
|
|
filterFn: "advancedText",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
accessorKey: "codigo_conta",
|
2025-10-20 21:45:09 +00:00
|
|
|
header: "Código da Conta",
|
2025-10-20 20:35:24 +00:00
|
|
|
filterFn: "advancedText",
|
2025-10-20 15:15:53 +00:00
|
|
|
},
|
2025-10-20 21:57:27 +00:00
|
|
|
{
|
|
|
|
|
accessorKey: "conta",
|
|
|
|
|
header: "Nome da Conta",
|
|
|
|
|
filterFn: "advancedText",
|
2025-10-20 21:45:09 +00:00
|
|
|
},
|
2025-10-20 20:35:24 +00:00
|
|
|
{
|
|
|
|
|
accessorKey: "valor",
|
2025-10-20 21:45:09 +00:00
|
|
|
header: "Valor Realizado",
|
2025-10-20 16:03:14 +00:00
|
|
|
filterFn: "advancedText",
|
|
|
|
|
cell: ({ getValue }: { getValue: () => number }) => {
|
2025-10-20 15:15:53 +00:00
|
|
|
const value = getValue();
|
2025-10-20 20:35:24 +00:00
|
|
|
const formatted = new Intl.NumberFormat("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
2025-10-20 15:15:53 +00:00
|
|
|
}).format(value);
|
|
|
|
|
const isNegative = value < 0;
|
|
|
|
|
return (
|
2025-10-20 20:35:24 +00:00
|
|
|
<span className={isNegative ? "text-red-600" : "text-gray-900"}>
|
2025-10-20 15:15:53 +00:00
|
|
|
{formatted}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
2025-10-20 20:35:24 +00:00
|
|
|
},
|
|
|
|
|
},
|
2025-10-20 21:45:09 +00:00
|
|
|
{
|
|
|
|
|
accessorKey: "valor_previsto",
|
|
|
|
|
header: "Valor Previsto",
|
|
|
|
|
filterFn: "advancedText",
|
2025-10-21 13:08:00 +00:00
|
|
|
cell: ({ getValue }: { getValue: () => number }) => {
|
|
|
|
|
const value = getValue();
|
|
|
|
|
if (!value || value === 0) return "-";
|
|
|
|
|
const formatted = new Intl.NumberFormat("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
|
|
|
|
}).format(value);
|
|
|
|
|
const isNegative = value < 0;
|
|
|
|
|
return (
|
|
|
|
|
<span className={isNegative ? "text-red-600" : "text-gray-900"}>
|
|
|
|
|
{formatted}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
},
|
2025-10-20 21:45:09 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
accessorKey: "valor_confirmado",
|
|
|
|
|
header: "Valor Confirmado",
|
|
|
|
|
filterFn: "advancedText",
|
2025-10-21 13:08:00 +00:00
|
|
|
cell: ({ getValue }: { getValue: () => number }) => {
|
|
|
|
|
const value = getValue();
|
|
|
|
|
if (!value || value === 0) return "-";
|
|
|
|
|
const formatted = new Intl.NumberFormat("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
|
|
|
|
}).format(value);
|
|
|
|
|
const isNegative = value < 0;
|
|
|
|
|
return (
|
|
|
|
|
<span className={isNegative ? "text-red-600" : "text-gray-900"}>
|
|
|
|
|
{formatted}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
},
|
2025-10-20 21:45:09 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
accessorKey: "valor_pago",
|
|
|
|
|
header: "Valor Pago",
|
|
|
|
|
filterFn: "advancedText",
|
2025-10-21 13:08:00 +00:00
|
|
|
cell: ({ getValue }: { getValue: () => number }) => {
|
|
|
|
|
const value = getValue();
|
|
|
|
|
if (!value || value === 0) return "-";
|
|
|
|
|
const formatted = new Intl.NumberFormat("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
|
|
|
|
}).format(value);
|
|
|
|
|
const isNegative = value < 0;
|
|
|
|
|
return (
|
|
|
|
|
<span className={isNegative ? "text-red-600" : "text-gray-900"}>
|
|
|
|
|
{formatted}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
},
|
2025-10-20 21:45:09 +00:00
|
|
|
},
|
2025-10-20 20:35:24 +00:00
|
|
|
{
|
|
|
|
|
accessorKey: "historico",
|
|
|
|
|
header: "Histórico",
|
|
|
|
|
filterFn: "advancedText",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
accessorKey: "historico2",
|
|
|
|
|
header: "Histórico 2",
|
|
|
|
|
filterFn: "advancedText",
|
2025-10-20 15:15:53 +00:00
|
|
|
},
|
2025-10-20 21:57:27 +00:00
|
|
|
{
|
|
|
|
|
accessorKey: "numero_lancamento",
|
|
|
|
|
header: "Número do Lançamento",
|
2025-10-20 21:45:09 +00:00
|
|
|
filterFn: "advancedText",
|
2025-10-21 13:08:00 +00:00
|
|
|
cell: ({ getValue }: { getValue: () => number }) => {
|
|
|
|
|
const value = getValue();
|
|
|
|
|
return value || "-";
|
|
|
|
|
},
|
2025-10-20 21:45:09 +00:00
|
|
|
},
|
2025-10-20 15:15:53 +00:00
|
|
|
],
|
|
|
|
|
[]
|
|
|
|
|
);
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-20 15:15:53 +00:00
|
|
|
const filterFns = React.useMemo(
|
|
|
|
|
() => ({
|
2025-10-21 13:08:00 +00:00
|
|
|
advancedText: (row: any, columnId: string, filters: ColumnFiltersState) => {
|
2025-10-20 17:55:53 +00:00
|
|
|
if (!filters || filters.length === 0) return true;
|
2025-10-20 20:35:24 +00:00
|
|
|
|
2025-10-20 17:55:53 +00:00
|
|
|
// Se veio um único filtro (objeto), transforma em array
|
|
|
|
|
const conds = Array.isArray(filters) ? filters : [filters];
|
2025-10-20 20:35:24 +00:00
|
|
|
|
2025-10-20 17:55:53 +00:00
|
|
|
// A coluna deve atender a todas as condições aplicáveis a ela
|
|
|
|
|
return conds.every((filter) => {
|
|
|
|
|
const raw = row.getValue(columnId);
|
|
|
|
|
const v = raw == null ? "" : String(raw);
|
|
|
|
|
const op = filter.operator;
|
|
|
|
|
const q = (filter.value ?? "").toString();
|
|
|
|
|
const a = v.toLowerCase();
|
|
|
|
|
const b = q.toLowerCase();
|
2025-10-20 20:35:24 +00:00
|
|
|
|
2025-10-20 17:55:53 +00:00
|
|
|
switch (op) {
|
|
|
|
|
case "contains":
|
|
|
|
|
return a.includes(b);
|
|
|
|
|
case "equals":
|
|
|
|
|
return a === b;
|
|
|
|
|
case "startsWith":
|
|
|
|
|
return a.startsWith(b);
|
|
|
|
|
case "endsWith":
|
|
|
|
|
return a.endsWith(b);
|
|
|
|
|
case "empty":
|
|
|
|
|
return a.length === 0;
|
|
|
|
|
case "notEmpty":
|
|
|
|
|
return a.length > 0;
|
|
|
|
|
default:
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-10-20 15:15:53 +00:00
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
[]
|
|
|
|
|
);
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-20 15:15:53 +00:00
|
|
|
const table = useReactTable({
|
|
|
|
|
data,
|
2025-10-21 13:08:00 +00:00
|
|
|
columns: columns as any,
|
2025-10-20 15:15:53 +00:00
|
|
|
state: { globalFilter, columnFilters },
|
|
|
|
|
onGlobalFilterChange: setGlobalFilter,
|
|
|
|
|
onColumnFiltersChange: setColumnFilters,
|
|
|
|
|
filterFns,
|
|
|
|
|
getCoreRowModel: getCoreRowModel(),
|
|
|
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
|
|
|
getSortedRowModel: getSortedRowModel(),
|
2025-10-08 05:08:35 +00:00
|
|
|
});
|
|
|
|
|
|
2025-10-20 15:15:53 +00:00
|
|
|
const parentRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
|
const rowVirtualizer = useVirtualizer({
|
|
|
|
|
count: table.getRowModel().rows.length,
|
|
|
|
|
getScrollElement: () => parentRef.current,
|
|
|
|
|
estimateSize: () => 36,
|
|
|
|
|
overscan: 20,
|
|
|
|
|
});
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-20 15:15:53 +00:00
|
|
|
const virtualRows = rowVirtualizer.getVirtualItems();
|
|
|
|
|
|
|
|
|
|
const applyFilters = () => {
|
2025-10-20 17:55:53 +00:00
|
|
|
// Agrupar múltiplas condições por coluna
|
|
|
|
|
const grouped: Record<string, any[]> = {};
|
2025-10-20 20:35:24 +00:00
|
|
|
|
2025-10-20 17:55:53 +00:00
|
|
|
conditions.forEach((c) => {
|
|
|
|
|
if (
|
|
|
|
|
c.column &&
|
2025-10-20 20:35:24 +00:00
|
|
|
(c.operator === "empty" ||
|
|
|
|
|
c.operator === "notEmpty" ||
|
|
|
|
|
(c.value ?? "") !== "")
|
2025-10-20 17:55:53 +00:00
|
|
|
) {
|
|
|
|
|
if (!grouped[c.column]) grouped[c.column] = [];
|
|
|
|
|
grouped[c.column].push({ operator: c.operator, value: c.value });
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-10-20 20:35:24 +00:00
|
|
|
|
2025-10-20 17:55:53 +00:00
|
|
|
// Converte em formato aceito pelo TanStack
|
|
|
|
|
const filters = Object.keys(grouped).map((col) => ({
|
|
|
|
|
id: col,
|
|
|
|
|
value: grouped[col],
|
2025-10-20 16:03:14 +00:00
|
|
|
}));
|
2025-10-20 20:35:24 +00:00
|
|
|
|
2025-10-20 15:15:53 +00:00
|
|
|
setColumnFilters(filters);
|
|
|
|
|
setOpen(false);
|
2025-10-08 12:28:20 +00:00
|
|
|
};
|
|
|
|
|
|
2025-10-20 15:15:53 +00:00
|
|
|
const clearFilters = () => {
|
|
|
|
|
setConditions([{ column: "", operator: "contains", value: "" }]);
|
|
|
|
|
setColumnFilters([]);
|
2025-10-20 20:26:14 +00:00
|
|
|
setGlobalFilter("");
|
2025-10-21 13:08:00 +00:00
|
|
|
// Não limpar os filtros externos - eles vêm do teste.tsx
|
2025-10-08 05:08:35 +00:00
|
|
|
};
|
|
|
|
|
|
2025-10-20 20:43:04 +00:00
|
|
|
const [totalValor, setTotalValor] = React.useState(0);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
// Usar dados filtrados da tabela em vez dos dados originais
|
2025-10-20 21:57:27 +00:00
|
|
|
const filteredData = table.getRowModel().rows.map((row) => row.original);
|
2025-10-20 20:43:04 +00:00
|
|
|
const newTotal = filteredData.reduce((sum, item) => {
|
2025-10-21 13:08:00 +00:00
|
|
|
const valor =
|
2025-10-20 20:43:04 +00:00
|
|
|
typeof item.valor === "string" ? parseFloat(item.valor) : item.valor;
|
2025-10-20 21:57:27 +00:00
|
|
|
return sum + (isNaN(valor) ? 0 : valor);
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
console.log("🔄 Calculando total:", {
|
2025-10-20 20:43:04 +00:00
|
|
|
totalRows: table.getRowModel().rows.length,
|
|
|
|
|
originalDataLength: data.length,
|
|
|
|
|
newTotal,
|
|
|
|
|
columnFilters: columnFilters.length,
|
2025-10-20 21:57:27 +00:00
|
|
|
globalFilter,
|
2025-10-20 20:43:04 +00:00
|
|
|
});
|
2025-10-20 21:57:27 +00:00
|
|
|
|
2025-10-20 20:43:04 +00:00
|
|
|
setTotalValor(newTotal);
|
|
|
|
|
}, [table, data, columnFilters, globalFilter]);
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-21 02:12:40 +00:00
|
|
|
// Calcular totais das colunas de valores para o footer - EXATAMENTE o mesmo padrão do Valor Total
|
2025-10-20 21:57:27 +00:00
|
|
|
const columnTotals = React.useMemo(() => {
|
2025-10-21 02:12:40 +00:00
|
|
|
// Usar EXATAMENTE a mesma lógica do totalValor
|
2025-10-20 21:57:27 +00:00
|
|
|
const filteredData = table.getRowModel().rows.map((row) => row.original);
|
|
|
|
|
const valorRealizado = filteredData.reduce((sum, item) => {
|
2025-10-21 13:08:00 +00:00
|
|
|
const valor = typeof item.valor === "string" ? parseFloat(item.valor) : item.valor;
|
|
|
|
|
return sum + (isNaN(valor) ? 0 : valor);
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
const valorPrevisto = filteredData.reduce((sum, item) => {
|
|
|
|
|
const valor = typeof item.valor_previsto === "string" ? parseFloat(item.valor_previsto) : (item.valor_previsto || 0);
|
|
|
|
|
return sum + (isNaN(valor) ? 0 : valor);
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
const valorConfirmado = filteredData.reduce((sum, item) => {
|
|
|
|
|
const valor = typeof item.valor_confirmado === "string" ? parseFloat(item.valor_confirmado) : (item.valor_confirmado || 0);
|
|
|
|
|
return sum + (isNaN(valor) ? 0 : valor);
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
const valorPago = filteredData.reduce((sum, item) => {
|
|
|
|
|
const valor = typeof item.valor_pago === "string" ? parseFloat(item.valor_pago) : (item.valor_pago || 0);
|
2025-10-21 02:43:13 +00:00
|
|
|
return sum + (isNaN(valor) ? 0 : valor);
|
|
|
|
|
}, 0);
|
2025-10-20 21:57:27 +00:00
|
|
|
|
2025-10-21 13:23:58 +00:00
|
|
|
console.log("🔄 Calculando totais das colunas:", {
|
|
|
|
|
totalRows: table.getRowModel().rows.length,
|
|
|
|
|
valorRealizado,
|
|
|
|
|
valorPrevisto,
|
|
|
|
|
valorConfirmado,
|
|
|
|
|
valorPago,
|
|
|
|
|
columnFilters: columnFilters.length,
|
|
|
|
|
globalFilter,
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-20 21:57:27 +00:00
|
|
|
return {
|
|
|
|
|
valorRealizado,
|
2025-10-21 13:08:00 +00:00
|
|
|
valorPrevisto,
|
|
|
|
|
valorConfirmado,
|
|
|
|
|
valorPago,
|
2025-10-20 21:57:27 +00:00
|
|
|
};
|
2025-10-21 13:23:58 +00:00
|
|
|
}, [table, columnFilters, globalFilter]);
|
2025-10-20 21:57:27 +00:00
|
|
|
|
2025-10-08 11:59:57 +00:00
|
|
|
const exportToExcel = () => {
|
|
|
|
|
if (data.length === 0) return;
|
|
|
|
|
|
2025-10-20 20:53:57 +00:00
|
|
|
// Usar dados filtrados da tabela em vez dos dados originais
|
2025-10-20 21:57:27 +00:00
|
|
|
const filteredData = table.getRowModel().rows.map((row) => row.original);
|
|
|
|
|
|
2025-10-20 20:53:57 +00:00
|
|
|
if (filteredData.length === 0) {
|
2025-10-20 21:57:27 +00:00
|
|
|
alert("Nenhum dado filtrado para exportar");
|
2025-10-20 20:53:57 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 11:59:57 +00:00
|
|
|
// Preparar dados para exportação
|
2025-10-20 20:53:57 +00:00
|
|
|
const exportData = filteredData.map((item) => ({
|
2025-10-20 20:35:24 +00:00
|
|
|
"Data Competência": new Date(item.data_competencia).toLocaleDateString(
|
|
|
|
|
"pt-BR"
|
2025-10-08 12:14:39 +00:00
|
|
|
),
|
2025-10-20 20:35:24 +00:00
|
|
|
"Data Vencimento": new Date(item.data_vencimento).toLocaleDateString(
|
|
|
|
|
"pt-BR"
|
2025-10-08 11:59:57 +00:00
|
|
|
),
|
2025-10-20 20:35:24 +00:00
|
|
|
"Data Caixa": new Date(item.data_caixa).toLocaleDateString("pt-BR"),
|
|
|
|
|
"Código Fornecedor": item.codigo_fornecedor,
|
2025-10-08 12:11:22 +00:00
|
|
|
Fornecedor: item.nome_fornecedor,
|
2025-10-20 20:35:24 +00:00
|
|
|
"Código Centro Custo": item.codigo_centrocusto,
|
|
|
|
|
"Centro Custo": item.codigo_centrocusto, // Assumindo que é o mesmo valor
|
|
|
|
|
"Código Conta": item.codigo_conta,
|
2025-10-08 11:59:57 +00:00
|
|
|
Conta: item.conta,
|
|
|
|
|
Valor:
|
2025-10-20 20:35:24 +00:00
|
|
|
typeof item.valor === "string" ? parseFloat(item.valor) : item.valor,
|
2025-10-08 11:59:57 +00:00
|
|
|
Histórico: item.historico,
|
2025-10-20 20:35:24 +00:00
|
|
|
"Histórico 2": item.historico2,
|
2025-10-08 12:11:22 +00:00
|
|
|
Recnum: item.recnum,
|
2025-10-08 11:59:57 +00:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Criar workbook
|
|
|
|
|
const wb = XLSX.utils.book_new();
|
|
|
|
|
const ws = XLSX.utils.json_to_sheet(exportData);
|
|
|
|
|
|
|
|
|
|
// Adicionar resumo na segunda aba
|
|
|
|
|
const resumoData = [
|
2025-10-20 20:53:57 +00:00
|
|
|
{ Métrica: "Total de Registros", Valor: filteredData.length },
|
2025-10-20 20:35:24 +00:00
|
|
|
{ Métrica: "Valor Total", Valor: totalValor },
|
2025-10-20 21:57:27 +00:00
|
|
|
{
|
|
|
|
|
Métrica: "Filtros Aplicados",
|
|
|
|
|
Valor: columnFilters.length > 0 || globalFilter ? "Sim" : "Não",
|
|
|
|
|
},
|
2025-10-08 11:59:57 +00:00
|
|
|
];
|
|
|
|
|
const wsResumo = XLSX.utils.json_to_sheet(resumoData);
|
|
|
|
|
|
|
|
|
|
// Adicionar abas ao workbook
|
2025-10-20 20:35:24 +00:00
|
|
|
XLSX.utils.book_append_sheet(wb, ws, "Dados Analíticos");
|
|
|
|
|
XLSX.utils.book_append_sheet(wb, wsResumo, "Resumo");
|
2025-10-08 11:59:57 +00:00
|
|
|
|
|
|
|
|
// Gerar nome do arquivo com data e hora
|
|
|
|
|
const now = new Date();
|
2025-10-20 20:35:24 +00:00
|
|
|
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, "-");
|
2025-10-20 20:53:57 +00:00
|
|
|
const hasFilters = columnFilters.length > 0 || globalFilter;
|
2025-10-20 21:57:27 +00:00
|
|
|
const fileName = `analitico${
|
|
|
|
|
hasFilters ? "_filtrado" : ""
|
|
|
|
|
}_${timestamp}.xlsx`;
|
2025-10-08 11:59:57 +00:00
|
|
|
|
|
|
|
|
// Fazer download
|
|
|
|
|
XLSX.writeFile(wb, fileName);
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-08 05:08:35 +00:00
|
|
|
return (
|
2025-10-20 21:05:59 +00:00
|
|
|
<div className="w-full max-w-none mx-auto p-6">
|
2025-10-20 20:03:15 +00:00
|
|
|
{/* Header Section */}
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<div className="flex items-center gap-3 mb-4">
|
2025-10-20 20:35:24 +00:00
|
|
|
{/* <div className="w-12 h-12 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">
|
|
|
|
|
<svg
|
|
|
|
|
className="w-7 h-7 text-white"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
|
|
|
|
/>
|
2025-10-20 20:03:15 +00:00
|
|
|
</svg>
|
2025-10-20 20:35:24 +00:00
|
|
|
</div> */}
|
2025-10-20 20:03:15 +00:00
|
|
|
<div>
|
2025-10-20 20:35:24 +00:00
|
|
|
<h1 className="text-2xl font-bold text-gray-900">
|
|
|
|
|
Análise Analítica
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="text-sm text-gray-500">
|
|
|
|
|
Relatório detalhado de transações
|
|
|
|
|
</p>
|
2025-10-08 05:08:35 +00:00
|
|
|
</div>
|
2025-10-20 20:03:15 +00:00
|
|
|
</div>
|
2025-10-20 20:35:24 +00:00
|
|
|
|
2025-10-21 13:08:00 +00:00
|
|
|
{/* Filtros Externos Ativos */}
|
|
|
|
|
{(filtrosExternos.dataInicio || filtrosExternos.centroCusto || filtrosExternos.codigoGrupo || filtrosExternos.codigoConta) && (
|
|
|
|
|
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
|
|
|
|
<div className="flex items-center gap-2 mb-2">
|
|
|
|
|
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
|
|
|
|
<span className="text-sm font-medium text-blue-900">Filtros aplicados pela tabela DRE Gerencial:</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-wrap gap-2 text-xs text-blue-800">
|
|
|
|
|
{filtrosExternos.dataInicio && filtrosExternos.dataFim && (
|
|
|
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
|
|
|
Período: {filtrosExternos.dataInicio} a {filtrosExternos.dataFim}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{filtrosExternos.centroCusto && (
|
|
|
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
|
|
|
Centro: {filtrosExternos.centroCusto}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{filtrosExternos.codigoGrupo && (
|
|
|
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
|
|
|
Grupo: {filtrosExternos.codigoGrupo}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{filtrosExternos.codigoConta && (
|
|
|
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
|
|
|
Conta: {filtrosExternos.codigoConta}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-10-20 20:35:24 +00:00
|
|
|
{/* Controls */}
|
|
|
|
|
<div className="flex gap-3 flex-wrap">
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Filtrar tudo..."
|
|
|
|
|
value={globalFilter ?? ""}
|
|
|
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
|
|
|
setGlobalFilter(e.target.value)
|
|
|
|
|
}
|
|
|
|
|
className="w-64 bg-white border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => setOpen(true)}
|
|
|
|
|
className="bg-white border-gray-300 hover:bg-blue-50 hover:border-blue-300 text-gray-700"
|
|
|
|
|
>
|
|
|
|
|
<Filter className="w-4 h-4 mr-2" />
|
|
|
|
|
Filtros Avançados
|
|
|
|
|
{columnFilters.length > 0 && (
|
|
|
|
|
<span className="ml-2 bg-blue-600 text-white text-xs px-2 py-1 rounded-full">
|
|
|
|
|
{columnFilters.length}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
{(columnFilters.length > 0 || globalFilter) && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={clearFilters}
|
|
|
|
|
className="bg-white border-gray-300 hover:bg-red-50 hover:border-red-300 text-gray-700"
|
|
|
|
|
>
|
|
|
|
|
<X className="w-4 h-4 mr-2" />
|
2025-10-21 13:08:00 +00:00
|
|
|
Limpar Filtros Avançados
|
2025-10-20 20:35:24 +00:00
|
|
|
</Button>
|
|
|
|
|
)}
|
2025-10-21 02:04:42 +00:00
|
|
|
{data.length > 0 && (
|
|
|
|
|
<Button
|
|
|
|
|
onClick={exportToExcel}
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
2025-10-20 20:35:24 +00:00
|
|
|
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-green-50 hover:border-green-300 text-gray-700"
|
2025-10-21 02:04:42 +00:00
|
|
|
>
|
|
|
|
|
<Download className="h-4 w-4" />
|
|
|
|
|
Exportar XLSX
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2025-10-20 20:35:24 +00:00
|
|
|
</div>
|
2025-10-20 16:03:14 +00:00
|
|
|
</div>
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-20 20:03:15 +00:00
|
|
|
{/* Table Container */}
|
2025-10-20 20:07:31 +00:00
|
|
|
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-x-auto">
|
2025-10-21 13:25:58 +00:00
|
|
|
<div className="min-w-[2720px]">
|
2025-10-20 20:35:24 +00:00
|
|
|
{/* Table Header */}
|
|
|
|
|
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200 sticky top-0 z-20">
|
2025-10-20 21:45:09 +00:00
|
|
|
<div className="flex items-center px-4 py-3 text-xs font-semibold text-gray-700 uppercase tracking-wide">
|
2025-10-20 21:57:27 +00:00
|
|
|
<div className="w-[140px] whitespace-nowrap">
|
|
|
|
|
Data de Vencimento
|
|
|
|
|
</div>
|
2025-10-20 21:45:09 +00:00
|
|
|
<div className="w-[120px] whitespace-nowrap">Data de Caixa</div>
|
2025-10-21 14:23:00 +00:00
|
|
|
<div className="w-[120px] whitespace-nowrap">Entidade</div>
|
|
|
|
|
<div className="w-[120px] whitespace-nowrap">
|
|
|
|
|
Cód. Fornec
|
2025-10-20 21:57:27 +00:00
|
|
|
</div>
|
|
|
|
|
<div className="w-[220px] whitespace-nowrap">
|
|
|
|
|
Nome do Fornecedor
|
|
|
|
|
</div>
|
2025-10-20 21:45:09 +00:00
|
|
|
<div className="w-[140px] whitespace-nowrap">Centro de Custo</div>
|
|
|
|
|
<div className="w-[130px] whitespace-nowrap">Código da Conta</div>
|
|
|
|
|
<div className="w-[160px] whitespace-nowrap">Nome da Conta</div>
|
2025-10-20 21:57:27 +00:00
|
|
|
<div className="w-[130px] whitespace-nowrap text-right">
|
|
|
|
|
Valor Realizado
|
|
|
|
|
</div>
|
|
|
|
|
<div className="w-[120px] whitespace-nowrap text-right">
|
|
|
|
|
Valor Previsto
|
|
|
|
|
</div>
|
|
|
|
|
<div className="w-[130px] whitespace-nowrap text-right">
|
|
|
|
|
Valor Confirmado
|
|
|
|
|
</div>
|
2025-10-21 13:23:58 +00:00
|
|
|
<div className="w-[140px] whitespace-nowrap text-right">
|
2025-10-20 21:57:27 +00:00
|
|
|
Valor Pago
|
|
|
|
|
</div>
|
2025-10-21 13:25:58 +00:00
|
|
|
<div className="w-[20px] whitespace-nowrap"></div>
|
2025-10-21 14:23:00 +00:00
|
|
|
<div className="w-[320px] whitespace-nowrap">Histórico</div>
|
|
|
|
|
<div className="w-[400px] whitespace-nowrap">Histórico 2</div>
|
2025-10-20 21:57:27 +00:00
|
|
|
<div className="w-[50px] whitespace-nowrap">
|
|
|
|
|
Número do Lançamento
|
|
|
|
|
</div>
|
2025-10-20 20:03:15 +00:00
|
|
|
</div>
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 20:03:15 +00:00
|
|
|
|
2025-10-20 20:35:24 +00:00
|
|
|
{/* Table Body */}
|
|
|
|
|
<div
|
|
|
|
|
ref={parentRef}
|
2025-10-20 21:45:09 +00:00
|
|
|
className="max-h-[250px] overflow-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 hover:scrollbar-thumb-gray-400"
|
2025-10-20 20:35:24 +00:00
|
|
|
style={{
|
|
|
|
|
scrollbarWidth: "thin",
|
|
|
|
|
scrollbarColor: "#cbd5e0 #f7fafc",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="flex items-center justify-center h-64">
|
|
|
|
|
<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>
|
|
|
|
|
<p className="text-gray-500">Carregando dados...</p>
|
2025-10-20 21:57:27 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-20 20:35:24 +00:00
|
|
|
) : virtualRows.length === 0 ? (
|
|
|
|
|
<div className="flex items-center justify-center h-64">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
|
|
|
<svg
|
|
|
|
|
className="w-8 h-8 text-gray-400"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
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>
|
2025-10-20 21:57:27 +00:00
|
|
|
</div>
|
2025-10-20 20:35:24 +00:00
|
|
|
<p className="text-gray-500">Nenhum dado encontrado</p>
|
2025-10-20 21:57:27 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-20 20:35:24 +00:00
|
|
|
) : (
|
|
|
|
|
<div
|
|
|
|
|
className="relative"
|
|
|
|
|
style={{ height: `${rowVirtualizer.getTotalSize()}px` }}
|
|
|
|
|
>
|
|
|
|
|
{virtualRows.map((virtualRow) => {
|
|
|
|
|
const row = table.getRowModel().rows[virtualRow.index];
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={row.id}
|
2025-10-20 21:45:09 +00:00
|
|
|
className="absolute top-0 left-0 w-full flex items-center px-4 py-3 text-sm border-b border-gray-100 hover:bg-gray-50 transition-colors"
|
2025-10-20 20:35:24 +00:00
|
|
|
style={{ transform: `translateY(${virtualRow.start}px)` }}
|
|
|
|
|
>
|
2025-10-20 21:45:09 +00:00
|
|
|
<div className="w-[140px] text-gray-600 whitespace-nowrap">
|
2025-10-20 20:35:24 +00:00
|
|
|
{new Date(
|
|
|
|
|
row.original.data_vencimento
|
|
|
|
|
).toLocaleDateString("pt-BR")}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 21:45:09 +00:00
|
|
|
<div className="w-[120px] text-gray-600 whitespace-nowrap">
|
2025-10-20 20:35:24 +00:00
|
|
|
{new Date(row.original.data_caixa).toLocaleDateString(
|
|
|
|
|
"pt-BR"
|
|
|
|
|
)}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-21 14:23:00 +00:00
|
|
|
<div className="w-[120px] text-gray-500 whitespace-nowrap">
|
2025-10-21 13:08:00 +00:00
|
|
|
{row.original.entidade || "-"}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-21 14:23:00 +00:00
|
|
|
<div className="w-[120px] font-medium text-gray-900 whitespace-nowrap">
|
2025-10-20 20:35:24 +00:00
|
|
|
{row.original.codigo_fornecedor}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 20:35:24 +00:00
|
|
|
<div
|
2025-10-20 21:45:09 +00:00
|
|
|
className="w-[220px] text-gray-700 truncate"
|
2025-10-20 20:35:24 +00:00
|
|
|
title={row.original.nome_fornecedor}
|
|
|
|
|
>
|
|
|
|
|
{row.original.nome_fornecedor}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 21:45:09 +00:00
|
|
|
<div className="w-[140px] text-gray-600 whitespace-nowrap">
|
2025-10-20 20:35:24 +00:00
|
|
|
{row.original.codigo_centrocusto}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 21:45:09 +00:00
|
|
|
<div className="w-[130px] text-gray-600 whitespace-nowrap">
|
2025-10-20 20:35:24 +00:00
|
|
|
{row.original.codigo_conta}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 20:35:24 +00:00
|
|
|
<div
|
2025-10-20 21:45:09 +00:00
|
|
|
className="w-[160px] text-gray-700 truncate"
|
2025-10-20 20:35:24 +00:00
|
|
|
title={row.original.conta}
|
|
|
|
|
>
|
|
|
|
|
{row.original.conta}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 20:35:24 +00:00
|
|
|
<div
|
2025-10-20 21:45:09 +00:00
|
|
|
className={`w-[130px] text-right font-semibold whitespace-nowrap ${
|
2025-10-20 20:35:24 +00:00
|
|
|
row.original.valor < 0
|
|
|
|
|
? "text-red-600"
|
|
|
|
|
: "text-gray-900"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{new Intl.NumberFormat("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
|
|
|
|
}).format(row.original.valor)}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-21 13:08:00 +00:00
|
|
|
<div className="w-[120px] text-right whitespace-nowrap">
|
|
|
|
|
{row.original.valor_previsto && row.original.valor_previsto !== 0 ? (
|
|
|
|
|
<span className={`font-semibold ${
|
|
|
|
|
row.original.valor_previsto < 0 ? "text-red-600" : "text-gray-900"
|
|
|
|
|
}`}>
|
|
|
|
|
{new Intl.NumberFormat("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
|
|
|
|
}).format(row.original.valor_previsto)}
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-gray-500">-</span>
|
|
|
|
|
)}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-21 13:08:00 +00:00
|
|
|
<div className="w-[130px] text-right whitespace-nowrap">
|
|
|
|
|
{row.original.valor_confirmado && row.original.valor_confirmado !== 0 ? (
|
|
|
|
|
<span className={`font-semibold ${
|
|
|
|
|
row.original.valor_confirmado < 0 ? "text-red-600" : "text-gray-900"
|
|
|
|
|
}`}>
|
|
|
|
|
{new Intl.NumberFormat("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
|
|
|
|
}).format(row.original.valor_confirmado)}
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-gray-500">-</span>
|
|
|
|
|
)}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-21 13:23:58 +00:00
|
|
|
<div className="w-[140px] text-right whitespace-nowrap">
|
2025-10-21 13:08:00 +00:00
|
|
|
{row.original.valor_pago && row.original.valor_pago !== 0 ? (
|
|
|
|
|
<span className={`font-semibold ${
|
|
|
|
|
row.original.valor_pago < 0 ? "text-red-600" : "text-gray-900"
|
|
|
|
|
}`}>
|
|
|
|
|
{new Intl.NumberFormat("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
|
|
|
|
}).format(row.original.valor_pago)}
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-gray-500">-</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-10-21 13:25:58 +00:00
|
|
|
<div className="w-[20px] whitespace-nowrap"></div>
|
2025-10-20 21:57:27 +00:00
|
|
|
<div
|
2025-10-21 14:23:00 +00:00
|
|
|
className="w-[320px] text-gray-700 truncate"
|
2025-10-20 21:57:27 +00:00
|
|
|
title={row.original.historico}
|
|
|
|
|
>
|
2025-10-20 21:45:09 +00:00
|
|
|
{row.original.historico}
|
2025-10-21 13:08:00 +00:00
|
|
|
</div>
|
2025-10-20 21:57:27 +00:00
|
|
|
<div
|
2025-10-21 14:23:00 +00:00
|
|
|
className="w-[400px] text-gray-700 truncate"
|
2025-10-20 21:57:27 +00:00
|
|
|
title={row.original.historico2}
|
|
|
|
|
>
|
2025-10-20 21:45:09 +00:00
|
|
|
{row.original.historico2}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 21:57:27 +00:00
|
|
|
<div className="w-[50px] text-gray-500 whitespace-nowrap">
|
2025-10-21 13:08:00 +00:00
|
|
|
{row.original.numero_lancamento || "-"}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-20 20:35:24 +00:00
|
|
|
);
|
|
|
|
|
})}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 20:35:24 +00:00
|
|
|
)}
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 20:26:14 +00:00
|
|
|
|
2025-10-20 21:57:27 +00:00
|
|
|
{/* Footer com Totalizador das Colunas */}
|
2025-10-20 20:35:24 +00:00
|
|
|
{data.length > 0 && (
|
2025-10-20 21:57:27 +00:00
|
|
|
<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
|
2025-10-20 20:43:04 +00:00
|
|
|
</div>
|
2025-10-20 21:57:27 +00:00
|
|
|
<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)}
|
2025-10-20 20:26:14 +00:00
|
|
|
</div>
|
2025-10-21 13:08:00 +00:00
|
|
|
<div className="w-[120px] text-right whitespace-nowrap">
|
|
|
|
|
{columnTotals.valorPrevisto !== 0 ? (
|
|
|
|
|
<span className={`font-bold ${
|
|
|
|
|
columnTotals.valorPrevisto < 0 ? "text-red-600" : "text-green-600"
|
|
|
|
|
}`}>
|
|
|
|
|
{new Intl.NumberFormat("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
|
|
|
|
}).format(columnTotals.valorPrevisto)}
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-gray-500">-</span>
|
|
|
|
|
)}
|
2025-10-20 20:26:14 +00:00
|
|
|
</div>
|
2025-10-21 13:08:00 +00:00
|
|
|
<div className="w-[130px] text-right whitespace-nowrap">
|
|
|
|
|
{columnTotals.valorConfirmado !== 0 ? (
|
|
|
|
|
<span className={`font-bold ${
|
|
|
|
|
columnTotals.valorConfirmado < 0 ? "text-red-600" : "text-green-600"
|
|
|
|
|
}`}>
|
|
|
|
|
{new Intl.NumberFormat("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
|
|
|
|
}).format(columnTotals.valorConfirmado)}
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-gray-500">-</span>
|
|
|
|
|
)}
|
2025-10-20 20:43:04 +00:00
|
|
|
</div>
|
2025-10-21 13:23:58 +00:00
|
|
|
<div className="w-[140px] text-right whitespace-nowrap">
|
2025-10-21 13:08:00 +00:00
|
|
|
{columnTotals.valorPago !== 0 ? (
|
|
|
|
|
<span className={`font-bold ${
|
|
|
|
|
columnTotals.valorPago < 0 ? "text-red-600" : "text-green-600"
|
|
|
|
|
}`}>
|
|
|
|
|
{new Intl.NumberFormat("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
|
|
|
|
}).format(columnTotals.valorPago)}
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-gray-500">-</span>
|
|
|
|
|
)}
|
2025-10-20 21:57:27 +00:00
|
|
|
</div>
|
2025-10-21 13:25:58 +00:00
|
|
|
<div className="w-[20px] whitespace-nowrap"></div>
|
2025-10-20 21:57:27 +00:00
|
|
|
<div className="w-[200px] whitespace-nowrap"></div>
|
|
|
|
|
<div className="w-[200px] whitespace-nowrap"></div>
|
|
|
|
|
<div className="w-[50px] whitespace-nowrap"></div>
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 20:03:15 +00:00
|
|
|
</div>
|
2025-10-20 20:35:24 +00:00
|
|
|
)}
|
2025-10-20 21:57:27 +00:00
|
|
|
|
|
|
|
|
{/* 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">
|
|
|
|
|
// <div className="flex justify-between items-center">
|
|
|
|
|
// <div className="flex items-center gap-4">
|
|
|
|
|
// <div>
|
|
|
|
|
// <h3 className="text-lg font-bold text-gray-900">
|
|
|
|
|
// Total de Registros:{" "}
|
|
|
|
|
// <span className="text-blue-600">
|
|
|
|
|
// {table.getRowModel().rows.length}
|
|
|
|
|
// </span>
|
|
|
|
|
// </h3>
|
|
|
|
|
// <p className="text-sm text-gray-600">
|
|
|
|
|
// Transações encontradas
|
|
|
|
|
// </p>
|
|
|
|
|
// </div>
|
|
|
|
|
// </div>
|
|
|
|
|
// <div className="text-right">
|
|
|
|
|
// <h3 className="text-lg font-bold">
|
|
|
|
|
// <span
|
|
|
|
|
// className={
|
|
|
|
|
// totalValor < 0 ? "text-red-600" : "text-green-600"
|
|
|
|
|
// }
|
|
|
|
|
// >
|
|
|
|
|
// 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>
|
|
|
|
|
// )
|
|
|
|
|
}
|
2025-10-20 20:03:15 +00:00
|
|
|
</div>
|
2025-10-20 20:26:14 +00:00
|
|
|
</div>
|
2025-10-20 20:03:15 +00:00
|
|
|
|
|
|
|
|
{/* Advanced Filters Dialog */}
|
|
|
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
|
|
|
<DialogContent className="max-w-2xl w-full mx-4 bg-white">
|
|
|
|
|
<DialogHeader className="pb-4">
|
2025-10-20 20:35:24 +00:00
|
|
|
<DialogTitle className="text-xl font-semibold text-gray-900">
|
|
|
|
|
Filtros Avançados
|
|
|
|
|
</DialogTitle>
|
2025-10-21 13:08:00 +00:00
|
|
|
<p className="text-sm text-gray-600">
|
|
|
|
|
Estes filtros são aplicados sobre os dados já filtrados pela tabela DRE Gerencial.
|
|
|
|
|
</p>
|
2025-10-20 20:03:15 +00:00
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4 max-h-96 overflow-y-auto bg-white">
|
|
|
|
|
{conditions.map((cond, idx) => (
|
2025-10-20 20:35:24 +00:00
|
|
|
<div
|
|
|
|
|
key={idx}
|
|
|
|
|
className="flex gap-3 items-start p-4 bg-gray-50 rounded-lg border border-gray-200"
|
|
|
|
|
>
|
2025-10-20 20:03:15 +00:00
|
|
|
<div className="flex-1">
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
|
|
Coluna
|
|
|
|
|
</label>
|
|
|
|
|
<Select
|
|
|
|
|
value={cond.column}
|
|
|
|
|
onValueChange={(v: string) => {
|
|
|
|
|
const next = [...conditions];
|
|
|
|
|
next[idx].column = v;
|
|
|
|
|
setConditions(next);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-full bg-white border-gray-300">
|
|
|
|
|
<SelectValue placeholder="Selecione a coluna" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
2025-10-20 20:35:24 +00:00
|
|
|
{columns.map((col) => (
|
|
|
|
|
<SelectItem
|
|
|
|
|
key={col.accessorKey}
|
|
|
|
|
value={col.accessorKey}
|
|
|
|
|
>
|
|
|
|
|
{col.header}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
2025-10-20 20:03:15 +00:00
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 20:03:15 +00:00
|
|
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
|
|
Operador
|
|
|
|
|
</label>
|
|
|
|
|
<Select
|
|
|
|
|
value={cond.operator}
|
|
|
|
|
onValueChange={(v: string) => {
|
|
|
|
|
const next = [...conditions];
|
|
|
|
|
next[idx].operator = v;
|
2025-10-20 20:35:24 +00:00
|
|
|
if (v === "empty" || v === "notEmpty")
|
|
|
|
|
next[idx].value = "";
|
2025-10-20 20:03:15 +00:00
|
|
|
setConditions(next);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-full bg-white border-gray-300">
|
|
|
|
|
<SelectValue placeholder="Selecione o operador" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="contains">contém</SelectItem>
|
|
|
|
|
<SelectItem value="equals">igual a</SelectItem>
|
|
|
|
|
<SelectItem value="startsWith">começa com</SelectItem>
|
|
|
|
|
<SelectItem value="endsWith">termina com</SelectItem>
|
|
|
|
|
<SelectItem value="empty">está vazio</SelectItem>
|
|
|
|
|
<SelectItem value="notEmpty">não está vazio</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 20:03:15 +00:00
|
|
|
|
2025-10-20 20:35:24 +00:00
|
|
|
{!(
|
|
|
|
|
cond.operator === "empty" || cond.operator === "notEmpty"
|
|
|
|
|
) && (
|
2025-10-20 15:15:53 +00:00
|
|
|
<div className="flex-1">
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
2025-10-20 20:03:15 +00:00
|
|
|
Valor
|
2025-10-20 15:15:53 +00:00
|
|
|
</label>
|
2025-10-20 20:03:15 +00:00
|
|
|
<Input
|
|
|
|
|
value={cond.value}
|
|
|
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
2025-10-20 15:15:53 +00:00
|
|
|
const next = [...conditions];
|
2025-10-20 20:03:15 +00:00
|
|
|
next[idx].value = e.target.value;
|
2025-10-20 15:15:53 +00:00
|
|
|
setConditions(next);
|
|
|
|
|
}}
|
2025-10-20 20:03:15 +00:00
|
|
|
placeholder="Digite o valor"
|
|
|
|
|
className="w-full bg-white border-gray-300"
|
|
|
|
|
/>
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
2025-10-20 20:03:15 +00:00
|
|
|
)}
|
2025-10-20 15:15:53 +00:00
|
|
|
|
2025-10-20 20:03:15 +00:00
|
|
|
{conditions.length > 1 && (
|
|
|
|
|
<div className="flex items-end">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const next = conditions.filter((_, i) => i !== idx);
|
2025-10-20 15:15:53 +00:00
|
|
|
setConditions(next);
|
|
|
|
|
}}
|
2025-10-20 20:03:15 +00:00
|
|
|
className="mt-6 text-red-600 hover:text-red-700 hover:bg-red-50 border-red-200"
|
2025-10-20 15:15:53 +00:00
|
|
|
>
|
2025-10-20 20:03:15 +00:00
|
|
|
✕
|
|
|
|
|
</Button>
|
2025-10-21 02:04:42 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
2025-10-20 15:15:53 +00:00
|
|
|
</div>
|
2025-10-20 20:03:15 +00:00
|
|
|
))}
|
2025-10-20 15:15:53 +00:00
|
|
|
|
2025-10-20 20:03:15 +00:00
|
|
|
<div className="flex justify-center pt-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() =>
|
|
|
|
|
setConditions((prev) => [
|
|
|
|
|
...prev,
|
|
|
|
|
{ column: "", operator: "contains", value: "" },
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
className="flex items-center gap-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 border-blue-200"
|
2025-10-20 15:15:53 +00:00
|
|
|
>
|
2025-10-20 20:03:15 +00:00
|
|
|
<span className="text-lg">+</span>
|
|
|
|
|
Adicionar condição
|
2025-10-20 15:15:53 +00:00
|
|
|
</Button>
|
2025-10-20 20:03:15 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DialogFooter className="flex gap-3 pt-6 border-t border-gray-200">
|
2025-10-20 20:35:24 +00:00
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
2025-10-20 20:03:15 +00:00
|
|
|
onClick={clearFilters}
|
|
|
|
|
className="flex-1 border-gray-300 text-gray-700 hover:bg-gray-50"
|
|
|
|
|
>
|
2025-10-21 13:08:00 +00:00
|
|
|
Limpar filtros avançados
|
2025-10-20 20:03:15 +00:00
|
|
|
</Button>
|
2025-10-20 20:35:24 +00:00
|
|
|
<Button
|
2025-10-20 20:03:15 +00:00
|
|
|
onClick={applyFilters}
|
|
|
|
|
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
|
|
|
|
|
>
|
|
|
|
|
Aplicar filtros
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</div>
|
2025-10-08 05:08:35 +00:00
|
|
|
);
|
2025-10-20 20:35:24 +00:00
|
|
|
}
|