vendaweb-api/src/app/DRE/analitico.tsx

1457 lines
53 KiB
TypeScript
Raw Normal View History

2025-10-20 20:35:24 +00:00
"use client";
import * as React from "react";
import { DataGridPremium, GridToolbar, GridColDef, GridFilterModel } from "@mui/x-data-grid-premium";
import { LicenseInfo } from '@mui/x-license-pro';
// Garantir que a licença seja aplicada no componente
if (typeof window !== 'undefined') {
try {
const PERPETUAL_LICENSE_KEY = 'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y';
LicenseInfo.setLicenseKey(PERPETUAL_LICENSE_KEY);
console.log('✅ Licença MUI X aplicada no componente Analítico');
} catch (error) {
console.warn('⚠️ Erro ao aplicar licença no componente:', error);
}
}
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
2025-10-22 21:55:32 +00:00
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
2025-10-22 21:55:32 +00:00
import { Checkbox } from "@/components/ui/checkbox";
2025-11-10 18:43:15 +00:00
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Download, Filter, X, Search, ArrowUpDown, ArrowUp, ArrowDown, Maximize2, Minimize2 } from "lucide-react";
2025-10-20 20:35:24 +00:00
import * as XLSX from "xlsx";
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;
centro_custo?: string;
valor: number;
historico: string;
historico2: string;
created_at: string;
updated_at: string;
// 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;
// Novos campos
data_lancamento?: string;
data_compensacao?: string;
data_pagto?: string;
}
interface AnaliticoProps {
filtros: {
dataInicio: string;
dataFim: string;
centroCusto?: string;
codigoGrupo?: string;
codigoSubgrupo?: string;
codigoConta?: string;
linhaSelecionada?: string;
excluirCentroCusto?: string;
excluirCodigoConta?: string;
2025-10-24 21:27:48 +00:00
codigosCentrosCustoSelecionados?: string;
codigosContasSelecionadas?: string;
};
}
2025-10-22 21:55:32 +00:00
// Componente de filtro customizado estilo Excel
interface ExcelFilterProps {
column: GridColDef;
data: any[];
filteredData: any[]; // Dados filtrados para mostrar apenas valores disponíveis
2025-10-22 21:55:32 +00:00
onFilterChange: (field: string, values: string[]) => void;
onSortChange: (field: string, direction: 'asc' | 'desc' | null) => void;
currentFilter?: string[];
currentSort?: 'asc' | 'desc' | null;
}
const ExcelFilter: React.FC<ExcelFilterProps> = ({
column,
data,
filteredData,
2025-10-22 21:55:32 +00:00
onFilterChange,
onSortChange,
currentFilter = [],
currentSort = null,
}) => {
const [isOpen, setIsOpen] = React.useState(false);
const [searchTerm, setSearchTerm] = React.useState("");
const [selectedValues, setSelectedValues] = React.useState<string[]>(currentFilter);
const [selectAll, setSelectAll] = React.useState(false);
2025-10-24 15:01:01 +00:00
// Sincronizar selectedValues com currentFilter quando ele mudar
React.useEffect(() => {
setSelectedValues(currentFilter);
}, [currentFilter]);
// Obter valores únicos da coluna baseado nos dados filtrados
2025-10-22 21:55:32 +00:00
const uniqueValues = React.useMemo(() => {
const values = filteredData
2025-10-22 21:55:32 +00:00
.map((row) => {
const value = row[column.field];
if (value === null || value === undefined) return "";
return String(value);
})
.filter((value, index, self) => self.indexOf(value) === index && value !== "")
.sort();
return values;
}, [filteredData, column.field]);
2025-10-22 21:55:32 +00:00
// Filtrar valores baseado na busca
const filteredValues = React.useMemo(() => {
if (!searchTerm) return uniqueValues;
return uniqueValues.filter((value) =>
value.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [uniqueValues, searchTerm]);
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedValues(filteredValues);
2025-10-24 15:01:01 +00:00
setSelectAll(true);
2025-10-22 21:55:32 +00:00
} else {
setSelectedValues([]);
2025-10-24 15:01:01 +00:00
setSelectAll(false);
2025-10-22 21:55:32 +00:00
}
};
const handleValueToggle = (value: string, checked: boolean) => {
2025-10-24 15:01:01 +00:00
let newValues: string[];
2025-10-22 21:55:32 +00:00
if (checked) {
2025-10-24 15:01:01 +00:00
newValues = [...selectedValues, value];
2025-10-22 21:55:32 +00:00
} else {
2025-10-24 15:01:01 +00:00
newValues = selectedValues.filter((v) => v !== value);
2025-10-22 21:55:32 +00:00
}
2025-10-24 15:01:01 +00:00
setSelectedValues(newValues);
setSelectAll(newValues.length === filteredValues.length && filteredValues.length > 0);
2025-10-22 21:55:32 +00:00
};
const handleApply = () => {
onFilterChange(column.field, selectedValues);
setIsOpen(false);
};
const handleClear = () => {
setSelectedValues([]);
2025-10-22 22:20:44 +00:00
setSelectAll(false);
2025-10-22 21:55:32 +00:00
onFilterChange(column.field, []);
setIsOpen(false);
};
const handleSort = (direction: 'asc' | 'desc') => {
onSortChange(column.field, direction);
setIsOpen(false);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
variant="ghost"
size="sm"
2025-10-22 22:20:44 +00:00
className="h-6 w-6 p-0 hover:bg-gray-200 rounded-sm"
2025-10-22 21:55:32 +00:00
onClick={() => setIsOpen(true)}
>
2025-10-22 22:20:44 +00:00
<ArrowUpDown className="h-3 w-3 text-gray-600" />
2025-10-22 21:55:32 +00:00
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="text-sm font-medium">
Filtrar por "{column.headerName}"
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* Opções de ordenação */}
<div className="space-y-2">
<div className="text-xs font-medium text-gray-600">Ordenar</div>
<div className="flex space-x-2">
<Button
variant="ghost"
size="sm"
className="h-8 text-xs"
onClick={() => handleSort('asc')}
>
<ArrowUp className="h-3 w-3 mr-1" />
A a Z
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 text-xs"
onClick={() => handleSort('desc')}
>
<ArrowDown className="h-3 w-3 mr-1" />
Z a A
</Button>
</div>
</div>
<div className="border-t pt-2">
<Button
variant="ghost"
size="sm"
className="h-8 text-xs text-red-600 hover:text-red-700"
onClick={handleClear}
>
<X className="h-3 w-3 mr-1" />
Limpar Filtro
</Button>
</div>
{/* Barra de pesquisa */}
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="Pesquisar"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 h-8 text-sm"
/>
</div>
{/* Lista de valores com checkboxes */}
<div className="max-h-60 overflow-y-auto border rounded-md">
<div className="p-2">
<div className="flex items-center space-x-2 py-1">
<Checkbox
id="select-all"
checked={selectAll}
onCheckedChange={handleSelectAll}
/>
<label htmlFor="select-all" className="text-sm font-medium">
(Selecionar Tudo)
</label>
</div>
{filteredValues.map((value) => (
<div key={value} className="flex items-center space-x-2 py-1">
<Checkbox
id={`value-${value}`}
checked={selectedValues.includes(value)}
onCheckedChange={(checked: boolean) => handleValueToggle(value, checked)}
/>
<label htmlFor={`value-${value}`} className="text-sm">
{value}
</label>
</div>
))}
</div>
</div>
{/* Botões de ação */}
<DialogFooter className="flex space-x-2">
<Button variant="outline" size="sm" onClick={() => setIsOpen(false)}>
Cancelar
</Button>
<Button size="sm" onClick={handleApply}>
OK
</Button>
</DialogFooter>
</div>
</DialogContent>
</Dialog>
);
};
export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
const [data, setData] = React.useState<AnaliticoItem[]>([]);
const [loading, setLoading] = React.useState(false);
const [globalFilter, setGlobalFilter] = React.useState("");
const [open, setOpen] = React.useState(false);
2025-11-10 18:43:15 +00:00
const [drawerOpen, setDrawerOpen] = React.useState(false);
2025-10-22 21:55:32 +00:00
const [columnFilters, setColumnFilters] = React.useState<Record<string, string[]>>({});
const [columnSorts, setColumnSorts] = React.useState<Record<string, 'asc' | 'desc' | null>>({});
2025-10-20 20:35:24 +00:00
const [conditions, setConditions] = React.useState([
{ column: "", operator: "contains", value: "" },
]);
2025-10-24 15:01:01 +00:00
// Estados para o card de agregação customizado (simplificado)
const [aggregationCardRef, setAggregationCardRef] = React.useState<HTMLDivElement | null>(null);
// Estado para armazenar filtros externos (vindos do teste.tsx)
const [filtrosExternos, setFiltrosExternos] = React.useState(filtros);
2025-10-22 21:55:32 +00:00
// Funções para gerenciar filtros customizados
const handleColumnFilterChange = React.useCallback((field: string, values: string[]) => {
setColumnFilters(prev => ({
...prev,
[field]: values
}));
}, []);
const handleColumnSortChange = React.useCallback((field: string, direction: 'asc' | 'desc' | null) => {
setColumnSorts(prev => ({
...prev,
[field]: direction
}));
}, []);
// Função para contar filtros aplicados (apenas filtros internos do modal customizado)
2025-10-23 14:39:13 +00:00
const getFilterCount = React.useCallback(() => {
let count = 0;
// Contar filtros de coluna (filtros do modal customizado)
2025-10-23 14:39:13 +00:00
count += Object.keys(columnFilters).length;
// Contar filtro global (se aplicável)
2025-10-23 14:39:13 +00:00
if (globalFilter && globalFilter.trim() !== "") {
count += 1;
}
return count;
}, [columnFilters, globalFilter]);
2025-10-23 14:39:13 +00:00
// Função para limpar todos os filtros internos (mantém filtros externos)
2025-10-22 22:20:44 +00:00
const clearAllFilters = React.useCallback(() => {
setColumnFilters({});
setColumnSorts({});
setGlobalFilter("");
2025-10-22 22:20:44 +00:00
}, []);
// Atualizar filtros externos quando os props mudarem
React.useEffect(() => {
console.log('🔄 Analítico - useEffect dos filtros chamado');
console.log('📋 Filtros recebidos via props:', filtros);
setFiltrosExternos(filtros);
}, [filtros]);
const fetchData = React.useCallback(async () => {
console.log('🔄 Analítico - fetchData chamado');
console.log('📋 Filtros externos recebidos:', filtrosExternos);
if (!filtrosExternos.dataInicio || !filtrosExternos.dataFim) {
console.log('⚠️ Sem dataInicio ou dataFim, limpando dados');
setData([]);
return;
}
setLoading(true);
try {
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);
}
if (filtrosExternos.excluirCentroCusto) {
params.append('excluirCentroCusto', filtrosExternos.excluirCentroCusto);
}
if (filtrosExternos.excluirCodigoConta) {
params.append('excluirCodigoConta', filtrosExternos.excluirCodigoConta);
}
2025-10-24 21:27:48 +00:00
if (filtrosExternos.codigosCentrosCustoSelecionados) {
params.append('codigosCentrosCustoSelecionados', filtrosExternos.codigosCentrosCustoSelecionados);
}
if (filtrosExternos.codigosContasSelecionadas) {
params.append('codigosContasSelecionadas', filtrosExternos.codigosContasSelecionadas);
}
const url = `/api/analitico-oracle?${params.toString()}`;
console.log('🌐 Fazendo requisição para:', url);
const response = await fetch(url);
if (response.ok) {
const result = await response.json();
console.log('✅ Resposta da API recebida:', result.length, 'registros');
console.log('📝 Primeiros 2 registros:', result.slice(0, 2));
console.log('🔍 Verificando campos específicos:', {
data_lancamento: result[0]?.data_lancamento,
data_compensacao: result[0]?.data_compensacao,
data_vencimento: result[0]?.data_vencimento,
data_caixa: result[0]?.data_caixa,
data_pagto: result[0]?.data_pagto,
entidade: result[0]?.entidade,
tipo_parceiro: result[0]?.tipo_parceiro,
centro_custo: result[0]?.centro_custo,
valor: result[0]?.valor,
tipo_valor: typeof result[0]?.valor
});
setData(result as AnaliticoItem[]);
} else {
console.error("❌ Erro ao buscar dados:", await response.text());
}
} catch (error) {
console.error("❌ Erro ao buscar dados:", error);
} finally {
setLoading(false);
}
}, [filtrosExternos]);
React.useEffect(() => {
fetchData();
}, [fetchData]);
// Filtrar dados baseado nos filtros de coluna
const filteredData = React.useMemo(() => {
if (!data || data.length === 0) return data;
return data.filter((row) => {
return Object.entries(columnFilters).every(([field, filterValues]) => {
if (!filterValues || filterValues.length === 0) return true;
const cellValue = (row as any)[field];
const stringValue = cellValue === null || cellValue === undefined ? "" : String(cellValue);
return filterValues.includes(stringValue);
});
}).map((row, index) => ({
...row,
id: `filtered-${row.id || row.recnum || index}` // Garantir ID único e estável
}));
}, [data, columnFilters]);
2025-10-24 15:01:01 +00:00
// Função para renderizar header com filtro Excel
const renderHeaderWithFilter = React.useCallback((column: GridColDef) => {
return (params: any) => (
<div className="flex items-center justify-between w-full">
<span className="text-sm font-medium">{column.headerName}</span>
<div className="flex items-center">
<ExcelFilter
column={column}
data={data}
filteredData={filteredData}
onFilterChange={handleColumnFilterChange}
onSortChange={handleColumnSortChange}
currentFilter={columnFilters[column.field] || []}
currentSort={columnSorts[column.field] || null}
/>
</div>
</div>
);
}, [data, filteredData, columnFilters, columnSorts, handleColumnFilterChange, handleColumnSortChange]);
// Definir colunas do DataGridPro na ordem solicitada
2025-10-22 21:55:32 +00:00
const columns = React.useMemo(() => {
const dateCellRenderer = (params: any) => {
if (!params.value) return "-";
try {
return new Date(params.value).toLocaleDateString("pt-BR");
} catch (error) {
return params.value;
}
};
const currencyCellRenderer = (params: any, showZero: boolean = false) => {
const value = params.value;
if (value === null || value === undefined || value === "") return "-";
if (!showZero && value === 0) return "-";
const numValue = typeof value === "string" ? parseFloat(value) : Number(value);
if (isNaN(numValue)) return "-";
const formatted = new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(numValue);
return (
<span className={`text-sm font-semibold ${numValue < 0 ? "text-red-600" : "text-gray-900"}`}>
{formatted}
</span>
);
};
2025-10-22 21:55:32 +00:00
const baseColumns = [
{
field: "data_lancamento",
headerName: "Dt Lanc",
width: 120,
sortable: true,
resizable: true,
renderCell: dateCellRenderer,
},
{
field: "data_compensacao",
headerName: "Dt Comp",
width: 120,
sortable: true,
resizable: true,
renderCell: dateCellRenderer,
},
2025-10-22 21:55:32 +00:00
{
field: "data_vencimento",
headerName: "Dt Venc",
width: 120,
2025-10-22 21:55:32 +00:00
sortable: true,
resizable: true,
renderCell: dateCellRenderer,
2025-10-22 21:55:32 +00:00
},
{
field: "data_caixa",
headerName: "Dt Caixa",
width: 120,
sortable: true,
resizable: true,
renderCell: dateCellRenderer,
},
{
field: "data_pagto",
headerName: "Dt Pagto",
width: 120,
2025-10-22 21:55:32 +00:00
sortable: true,
resizable: true,
renderCell: dateCellRenderer,
2025-10-20 20:35:24 +00:00
},
{
field: "ano_mes_comp",
headerName: "Ano/Mês Comp",
width: 110,
sortable: true,
resizable: true,
renderCell: (params: any) => params.value || "-",
},
2025-10-22 21:55:32 +00:00
{
field: "entidade",
headerName: "Entidade",
width: 100,
sortable: true,
resizable: true,
renderCell: (params: any) => params.value || "-",
2025-10-20 20:35:24 +00:00
},
{
field: "tipo_parceiro",
headerName: "Tipo Parc",
width: 120,
sortable: true,
resizable: true,
renderCell: (params: any) => params.value || "-",
},
2025-10-22 21:55:32 +00:00
{
field: "codigo_fornecedor",
headerName: "Cod.Fornec",
width: 120,
2025-10-22 21:55:32 +00:00
sortable: true,
resizable: true,
2025-10-20 20:35:24 +00:00
},
2025-10-22 21:55:32 +00:00
{
field: "nome_fornecedor",
headerName: "Fornecedor",
flex: 1,
minWidth: 200,
sortable: true,
resizable: true,
2025-10-20 21:45:09 +00:00
},
2025-10-22 21:55:32 +00:00
{
field: "codigo_centrocusto",
headerName: "Cod.CC",
width: 100,
sortable: true,
resizable: true,
},
{
field: "centro_custo",
headerName: "Centro Custo",
flex: 1,
minWidth: 200,
2025-10-22 21:55:32 +00:00
sortable: true,
resizable: true,
renderCell: (params: any) => params.value || "-",
2025-10-20 21:45:09 +00:00
},
2025-10-22 21:55:32 +00:00
{
field: "codigo_conta",
headerName: "Cod.Conta",
width: 120,
2025-10-22 21:55:32 +00:00
sortable: true,
resizable: true,
},
2025-10-22 21:55:32 +00:00
{
field: "conta",
headerName: "Conta",
flex: 1,
minWidth: 180,
sortable: true,
resizable: true,
},
{
field: "valor",
headerName: "Vl.Realizado",
type: "number" as const,
width: 140,
sortable: true,
resizable: true,
renderCell: (params: any) => currencyCellRenderer(params, true),
2025-10-22 21:55:32 +00:00
},
{
field: "valor_previsto",
headerName: "Vl.Previsto",
type: "number" as const,
width: 130,
sortable: true,
resizable: true,
renderCell: (params: any) => currencyCellRenderer(params, false),
2025-10-22 21:55:32 +00:00
},
{
field: "valor_confirmado",
headerName: "Vl.Confirmado",
type: "number" as const,
width: 140,
sortable: true,
resizable: true,
renderCell: (params: any) => currencyCellRenderer(params, false),
2025-10-22 21:55:32 +00:00
},
{
field: "valor_pago",
headerName: "Vl.Pago",
type: "number" as const,
width: 120,
sortable: true,
resizable: true,
renderCell: (params: any) => currencyCellRenderer(params, false),
2025-10-22 21:55:32 +00:00
},
{
field: "historico",
headerName: "Historico",
flex: 1,
minWidth: 250,
sortable: true,
resizable: true,
},
{
field: "historico2",
headerName: "Historico 2",
flex: 1,
minWidth: 300,
sortable: true,
resizable: true,
},
{
field: "numero_lancamento",
headerName: "Num.Lanc",
width: 100,
2025-10-22 21:55:32 +00:00
sortable: true,
resizable: true,
renderCell: (params: any) => params.value || "-",
},
];
// Adicionar renderHeader com filtro Excel para todas as colunas
return baseColumns.map((col) => ({
...col,
2025-10-24 15:01:01 +00:00
renderHeader: renderHeaderWithFilter(col),
2025-10-22 21:55:32 +00:00
}));
2025-10-24 15:01:01 +00:00
}, [renderHeaderWithFilter]);
2025-10-22 22:20:44 +00:00
// Ordenar dados baseado na ordenação de coluna
const sortedAndFilteredData = React.useMemo(() => {
2025-10-23 14:54:52 +00:00
if (!filteredData || filteredData.length === 0) return filteredData;
2025-10-22 22:20:44 +00:00
const sortField = Object.keys(columnSorts).find(field => columnSorts[field] !== null);
2025-10-23 14:54:52 +00:00
if (!sortField || !columnSorts[sortField]) return filteredData;
2025-10-22 22:20:44 +00:00
2025-10-23 14:54:52 +00:00
return [...filteredData].sort((a, b) => {
2025-10-22 22:20:44 +00:00
const aValue = (a as any)[sortField];
const bValue = (b as any)[sortField];
// Converter para string para comparação
const aString = aValue === null || aValue === undefined ? "" : String(aValue);
const bString = bValue === null || bValue === undefined ? "" : String(bValue);
if (columnSorts[sortField] === 'asc') {
return aString.localeCompare(bString);
} else {
return bString.localeCompare(aString);
}
});
}, [filteredData, columnSorts]);
2025-10-23 14:39:13 +00:00
// Calcular valor total dos dados filtrados
const valorTotal = React.useMemo(() => {
return sortedAndFilteredData.reduce((sum, item) => sum + (Number(item.valor) || 0), 0);
}, [sortedAndFilteredData]);
2025-10-24 15:01:01 +00:00
// Calcular totais das colunas de valor para o card de agregação
const columnTotals = React.useMemo(() => {
return {
valor: sortedAndFilteredData.reduce((sum, item) => sum + (Number(item.valor) || 0), 0),
valor_previsto: sortedAndFilteredData.reduce((sum, item) => sum + (Number(item.valor_previsto) || 0), 0),
valor_confirmado: sortedAndFilteredData.reduce((sum, item) => sum + (Number(item.valor_confirmado) || 0), 0),
valor_pago: sortedAndFilteredData.reduce((sum, item) => sum + (Number(item.valor_pago) || 0), 0),
};
}, [sortedAndFilteredData]);
// Limpar filtros de colunas que não têm mais valores disponíveis
React.useEffect(() => {
const updatedFilters = { ...columnFilters };
let hasChanges = false;
Object.keys(columnFilters).forEach(field => {
const currentFilterValues = columnFilters[field] || [];
if (currentFilterValues.length === 0) return;
// Obter valores únicos disponíveis para esta coluna nos dados filtrados
const availableValues = filteredData
.map(row => {
const value = (row as any)[field];
return value === null || value === undefined ? "" : String(value);
})
.filter((value, index, self) => self.indexOf(value) === index && value !== "");
// Filtrar apenas os valores que ainda estão disponíveis
const validFilterValues = currentFilterValues.filter(value =>
availableValues.includes(value)
);
if (validFilterValues.length !== currentFilterValues.length) {
if (validFilterValues.length === 0) {
delete updatedFilters[field];
} else {
updatedFilters[field] = validFilterValues;
}
hasChanges = true;
}
});
if (hasChanges) {
setColumnFilters(updatedFilters);
}
}, [filteredData, columnFilters]);
// Exportação XLSX
2025-10-08 11:59:57 +00:00
const exportToExcel = () => {
2025-10-22 22:20:44 +00:00
if (sortedAndFilteredData.length === 0) return;
2025-10-08 11:59:57 +00:00
2025-10-22 22:20:44 +00:00
const exportData = sortedAndFilteredData.map((item) => ({
"DTLANC": item.data_lancamento
? new Date(item.data_lancamento).toLocaleDateString("pt-BR")
: "-",
"DTCOMPENSACAO": item.data_compensacao
? new Date(item.data_compensacao).toLocaleDateString("pt-BR")
: "-",
"DTVENC": item.data_vencimento
? new Date(item.data_vencimento).toLocaleDateString("pt-BR")
: "-",
"DTCAIXA": item.data_caixa
? new Date(item.data_caixa).toLocaleDateString("pt-BR")
: "-",
"DTPAGTO": item.data_pagto
? new Date(item.data_pagto).toLocaleDateString("pt-BR")
: "-",
"ANOMESCOMP": item.ano_mes_comp || "-",
"ENTIDADE": item.entidade || "-",
"TIPOPARCEIRO": item.tipo_parceiro || "-",
"CODFORNEC": item.codigo_fornecedor || "-",
"FORNECEDOR": item.nome_fornecedor || "-",
"CODIGOCENTROCUSTO": item.codigo_centrocusto || "-",
"CENTROCUSTO": item.centro_custo || "-",
"CODCONTA": item.codigo_conta || "-",
"CONTA": item.conta || "-",
"VLREALIZADO": typeof item.valor === "string" ? parseFloat(item.valor) : (item.valor || 0),
"VLPREVISTO": typeof item.valor_previsto === "string" ? parseFloat(item.valor_previsto) : (item.valor_previsto || 0),
"VLCONFIRMADO": typeof item.valor_confirmado === "string" ? parseFloat(item.valor_confirmado) : (item.valor_confirmado || 0),
"VLPAGO": typeof item.valor_pago === "string" ? parseFloat(item.valor_pago) : (item.valor_pago || 0),
"HISTORICO": item.historico || "-",
"HISTORICO2": item.historico2 || "-",
"NUMLANC": item.numero_lancamento || "-",
2025-10-08 11:59:57 +00:00
}));
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(exportData);
const resumoData = [
2025-10-23 14:39:13 +00:00
{ Métrica: "Total de Registros", Valor: sortedAndFilteredData.length },
{ Métrica: "Valor Total", Valor: valorTotal },
{ Métrica: "Filtros Aplicados", Valor: Object.keys(columnFilters).length > 0 ? "Sim" : "Não" },
2025-10-08 11:59:57 +00:00
];
const wsResumo = XLSX.utils.json_to_sheet(resumoData);
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
const now = new Date();
2025-10-20 20:35:24 +00:00
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, "-");
const fileName = `analitico_${timestamp}.xlsx`;
2025-10-08 11:59:57 +00:00
XLSX.writeFile(wb, fileName);
};
// Aplicar filtros avançados
const applyFilters = () => {
// Implementar lógica de filtros avançados se necessário
setOpen(false);
};
const clearFilters = () => {
setConditions([{ column: "", operator: "contains", value: "" }]);
setGlobalFilter("");
};
2025-11-10 18:43:15 +00:00
// Função para renderizar o conteúdo principal do componente (reutilizável)
const renderAnaliticoContent = (isMaximized: boolean = false) => {
return (
2025-11-10 18:43:15 +00:00
<>
{/* Filtros Externos Ativos - Apenas quando maximizado */}
{isMaximized && (filtrosExternos.dataInicio || filtrosExternos.centroCusto || filtrosExternos.codigoGrupo || filtrosExternos.codigoConta) && (
2025-11-10 18:43:15 +00:00
<div className="flex items-center gap-2 mb-4">
<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 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>
)}
{/* Controls - Apenas quando maximizado */}
{isMaximized && (
<div className="flex gap-2 flex-wrap mb-4">
{data.length > 0 && (
<Button
onClick={clearAllFilters}
variant="outline"
size="sm"
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-red-50 hover:border-red-300 text-gray-700"
>
<X className="h-4 w-4" />
Limpar Filtros
{getFilterCount() > 0 && (
<span className="bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center font-semibold">
{getFilterCount()}
</span>
)}
</Button>
)}
2025-11-10 18:43:15 +00:00
<Button
onClick={exportToExcel}
2025-11-10 18:43:15 +00:00
variant="outline"
size="sm"
disabled={sortedAndFilteredData.length === 0}
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-green-50 hover:border-green-300 text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
2025-11-10 18:43:15 +00:00
>
<Download className="h-4 w-4" />
Exportar XLSX
2025-11-10 18:43:15 +00:00
</Button>
</div>
)}
2025-11-10 18:43:15 +00:00
{/* DataGridPro */}
2025-11-10 18:43:15 +00:00
<Card className={`w-full shadow-lg rounded-2xl ${isMaximized ? 'h-[calc(96vh-280px)]' : 'h-[40vh]'}`} style={{ overflowAnchor: 'none' }}>
<CardContent className="p-4 h-full" style={{ overflowAnchor: 'none' }}>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-4">
<h2 className="text-lg font-semibold">
Total de Registros: <span className="text-blue-600">{sortedAndFilteredData.length}</span>
</h2>
<div className="text-sm text-gray-600">
Valor Total: <span className={`font-bold ${valorTotal < 0 ? 'text-red-600' : 'text-green-600'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(valorTotal)}
</span>
2025-11-10 18:43:15 +00:00
</div>
</div>
</div>
2025-11-10 18:43:15 +00:00
<div style={{ height: "calc(100% - 2rem)", width: "100%", position: "relative", display: "flex", flexDirection: "column" }}>
<DataGridPremium
key={`datagrid-${sortedAndFilteredData.length}-${Object.keys(columnFilters).length}`}
rows={sortedAndFilteredData}
columns={columns}
loading={loading}
disableRowSelectionOnClick
density="compact"
slots={{ toolbar: GridToolbar }}
disableColumnMenu={true}
disableColumnSorting={true}
pagination={false}
disableVirtualization={false}
getRowId={(row: any) => row.id || `row-${row.recnum || Math.random()}`}
sx={{
overflowAnchor: 'none',
2025-11-10 18:43:15 +00:00
height: "100%",
display: "flex",
flexDirection: "column",
"& .MuiDataGrid-root": {
display: "flex",
flexDirection: "column",
height: "100%",
flex: 1,
overflow: "hidden",
},
"& .MuiDataGrid-main": {
overflow: "hidden !important",
position: "relative",
height: "100%",
display: "flex",
flexDirection: "column",
flex: 1,
},
// Container dos headers - DEVE FICAR FIXO (não rola)
"& .MuiDataGrid-container--top": {
overflow: "hidden !important",
position: "relative",
zIndex: 100,
backgroundColor: "#f9fafb",
2025-11-10 18:43:15 +00:00
flexShrink: 0,
flexGrow: 0,
},
"& .MuiDataGrid-columnHeaders": {
position: "relative !important",
backgroundColor: "#f9fafb !important",
zIndex: 100,
borderBottom: "1px solid #e5e7eb",
},
2025-11-10 18:43:15 +00:00
"& .MuiDataGrid-columnHeader": {
backgroundColor: "#f9fafb !important",
},
"& .MuiDataGrid-columnHeaderRow": {
backgroundColor: "#f9fafb !important",
},
"& .MuiDataGrid-columnHeadersInner": {
backgroundColor: "#f9fafb !important",
},
"& .MuiDataGrid-cell": {
borderBottom: "1px solid #f0f0f0",
fontSize: "0.875rem",
},
2025-11-10 18:43:15 +00:00
// Container do virtualScroller - deve ter scroll e ocupar espaço restante
"& .MuiDataGrid-container--bottom": {
flex: 1,
overflow: "hidden !important",
position: "relative",
minHeight: 0,
display: "flex",
flexDirection: "column",
},
// Apenas o virtualScroller deve ter scroll
"& .MuiDataGrid-virtualScroller": {
2025-11-10 18:43:15 +00:00
overflowY: "auto !important",
overflowX: "auto !important",
height: "100% !important",
flex: 1,
overflowAnchor: 'none',
// Garantir que a barra de scroll seja visível
scrollbarWidth: "thin",
"&::-webkit-scrollbar": {
height: "8px",
width: "8px",
},
"&::-webkit-scrollbar-track": {
background: "#f1f1f1",
},
"&::-webkit-scrollbar-thumb": {
background: "#888",
borderRadius: "4px",
},
"&::-webkit-scrollbar-thumb:hover": {
background: "#555",
},
},
"& .MuiDataGrid-virtualScrollerContent": {
minWidth: "max-content",
},
"& .MuiDataGrid-row": {
minWidth: "max-content",
},
"& .MuiDataGrid-toolbarContainer": {
backgroundColor: "#f8fafc",
borderBottom: "1px solid #e5e7eb",
padding: "8px 16px",
},
2025-11-10 18:43:15 +00:00
"& .MuiDataGrid-scrollbar": {
display: "none",
},
// Ocultar todos os ícones nativos das colunas
"& .MuiDataGrid-columnHeaderMenuContainer": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderMenuButton": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderSortIcon": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderSortIconContainer": {
display: "none !important",
},
"& .MuiDataGrid-iconButtonContainer": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderSeparator": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderSortButton": {
display: "none !important",
},
// Ocultar qualquer ícone de menu adicional
"& .MuiDataGrid-menuIcon": {
display: "none !important",
},
"& .MuiDataGrid-menuIconButton": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderMenuIcon": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderMenuIconButton": {
display: "none !important",
},
"& .MuiDataGrid-menuContainer": {
display: "none !important",
},
"& .MuiDataGrid-menu": {
display: "none !important",
},
// Ocultar footer de paginação
"& .MuiDataGrid-footerContainer": {
display: "none !important",
},
"& .MuiDataGrid-pagination": {
display: "none !important",
},
"& .MuiTablePagination-root": {
display: "none !important",
},
// Garantir que nosso botão customizado apareça
"& .MuiDataGrid-columnHeaderTitleContainer": {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
},
}}
/>
2025-11-10 18:43:15 +00:00
{/* Card de Agregação Customizado */}
{sortedAndFilteredData.length > 0 && (
<div
ref={setAggregationCardRef}
className="w-full bg-gray-50 border-t border-gray-200 sticky bottom-0 z-50 shadow-lg"
style={{ overflowAnchor: 'none' }}
>
<div className="px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<div className="text-sm font-medium text-gray-700">
Vl.Realizado:
<span className={`ml-2 font-semibold ${columnTotals.valor < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valor)}
</span>
</div>
2025-11-10 18:43:15 +00:00
<div className="text-sm font-medium text-gray-700">
Vl.Previsto:
<span className={`ml-2 font-semibold ${columnTotals.valor_previsto < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valor_previsto)}
</span>
2025-11-10 18:43:15 +00:00
</div>
<div className="text-sm font-medium text-gray-700">
Vl.Confirmado:
<span className={`ml-2 font-semibold ${columnTotals.valor_confirmado < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valor_confirmado)}
</span>
</div>
<div className="text-sm font-medium text-gray-700">
Vl.Pago:
<span className={`ml-2 font-semibold ${columnTotals.valor_pago < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valor_pago)}
</span>
</div>
</div>
<div className="text-sm text-gray-500">
Total de Registros: <span className="font-semibold text-blue-600">{sortedAndFilteredData.length}</span>
</div>
</div>
</div>
</div>
)}
</div>
</CardContent>
</Card>
2025-11-10 18:43:15 +00:00
</>
);
};
return (
2025-10-24 15:01:01 +00:00
<div className="w-full max-w-none mx-auto p-2" style={{ overflowAnchor: 'none' }}>
{/* Header Section */}
2025-10-21 21:18:51 +00:00
<div className="mb-2">
<div className="flex items-center justify-between mb-1">
<div>
2025-10-20 20:35:24 +00:00
<h1 className="text-2xl font-bold text-gray-900">
2025-10-21 20:57:58 +00:00
Análise Analítica{filtros.linhaSelecionada ? ` - ${filtros.linhaSelecionada}` : ""}
2025-10-20 20:35:24 +00:00
</h1>
<p className="text-sm text-gray-500">
Relatório detalhado de transações
</p>
</div>
{/* Filtros Externos Ativos - Centralizado */}
{(filtrosExternos.dataInicio || filtrosExternos.centroCusto || filtrosExternos.codigoGrupo || filtrosExternos.codigoConta) && (
<div className="flex items-center gap-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 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-21 21:18:51 +00:00
{/* Controls */}
<div className="flex gap-2 flex-wrap">
2025-10-22 22:20:44 +00:00
{/* <Input
2025-10-21 21:18:51 +00:00
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
2025-10-21 21:18:51 +00:00
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
2025-10-22 22:20:44 +00:00
</Button> */}
{globalFilter && (
2025-10-21 21:18:51 +00:00
<Button
variant="outline"
onClick={clearFilters}
2025-10-23 14:39:13 +00:00
className="bg-white border-gray-300 hover:bg-red-50 hover:border-red-300 text-gray-700 flex items-center gap-2"
2025-10-21 21:18:51 +00:00
>
2025-10-23 14:39:13 +00:00
<X className="w-4 h-4" />
Limpar Filtros
2025-10-23 14:39:13 +00:00
{getFilterCount() > 0 && (
<span className="bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-semibold">
{getFilterCount()}
</span>
)}
2025-10-21 21:18:51 +00:00
</Button>
)}
<div className="flex items-center gap-2">
{data.length > 0 && (
<Button
2025-10-22 22:20:44 +00:00
onClick={clearAllFilters}
variant="outline"
size="sm"
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-red-50 hover:border-red-300 text-gray-700"
>
<X className="h-4 w-4" />
Limpar Filtros
2025-10-23 14:39:13 +00:00
{getFilterCount() > 0 && (
<span className="bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center font-semibold">
{getFilterCount()}
</span>
)}
</Button>
)}
<Button
2025-10-21 21:18:51 +00:00
onClick={exportToExcel}
variant="outline"
size="sm"
disabled={sortedAndFilteredData.length === 0}
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-green-50 hover:border-green-300 text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
2025-10-21 21:18:51 +00:00
>
<Download className="h-4 w-4" />
Exportar XLSX
</Button>
2025-11-10 18:43:15 +00:00
<Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
<DrawerTrigger asChild>
<Button
variant="outline"
size="sm"
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-blue-50 hover:border-blue-300 text-gray-700"
>
<Maximize2 className="h-4 w-4" />
Maximizar
</Button>
</DrawerTrigger>
<DrawerContent className="max-h-[96vh] h-[96vh]">
<DrawerHeader className="flex-shrink-0">
<div className="flex items-center justify-between">
<div className="flex-1">
<DrawerTitle className="text-2xl font-bold text-gray-900">
Análise Analítica{filtros.linhaSelecionada ? ` - ${filtros.linhaSelecionada}` : ""}
</DrawerTitle>
<DrawerDescription>
Relatório detalhado de transações - Versão Maximizada
</DrawerDescription>
</div>
<DrawerClose asChild>
<Button
variant="outline"
size="sm"
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-red-50 hover:border-red-300 text-gray-700"
>
<Minimize2 className="h-4 w-4" />
Minimizar
</Button>
</DrawerClose>
</div>
</DrawerHeader>
<div className="flex-1 overflow-y-auto px-4 pb-4">
{renderAnaliticoContent(true)}
</div>
</DrawerContent>
</Drawer>
</div>
</div>
</div>
2025-10-20 20:35:24 +00:00
</div>
2025-11-10 18:43:15 +00:00
{/* Conteúdo Principal - Versão Normal */}
{renderAnaliticoContent(false)}
{/* 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>
<p className="text-sm text-gray-600">
Estes filtros são aplicados sobre os dados filtrados pela tabela DRE Gerencial.
</p>
</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}
2025-10-21 21:18:51 +00:00
className="flex gap-2 items-start p-2 bg-gray-50 rounded-lg border border-gray-200"
2025-10-20 20:35:24 +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>
{columns.map((col: any) => (
2025-10-20 20:35:24 +00:00
<SelectItem
key={col.field}
value={col.field}
2025-10-20 20:35:24 +00:00
>
{col.headerName}
2025-10-20 20:35:24 +00:00
</SelectItem>
))}
</SelectContent>
</Select>
2025-10-24 15:01:01 +00:00
</div>
<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 = "";
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>
</div>
{!(cond.operator === "empty" || cond.operator === "notEmpty") && (
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
Valor
</label>
<Input
value={cond.value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const next = [...conditions];
next[idx].value = e.target.value;
setConditions(next);
}}
placeholder="Digite o valor"
className="w-full bg-white border-gray-300"
/>
</div>
)}
{conditions.length > 1 && (
<div className="flex items-end">
<Button
variant="outline"
size="sm"
onClick={() => {
const next = conditions.filter((_, i) => i !== idx);
setConditions(next);
}}
className="mt-6 text-red-600 hover:text-red-700 hover:bg-red-50 border-red-200"
>
</Button>
</div>
)}
</div>
))}
<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"
>
<span className="text-lg">+</span>
Adicionar condição
</Button>
</div>
</div>
2025-10-21 21:18:51 +00:00
<DialogFooter className="flex gap-2 pt-3 border-t border-gray-200">
<Button
2025-10-20 20:35:24 +00:00
variant="outline"
onClick={clearFilters}
2025-10-23 14:39:13 +00:00
className="flex-1 border-gray-300 text-gray-700 hover:bg-gray-50 flex items-center justify-center gap-2"
>
Limpar filtros avançados
2025-10-23 14:39:13 +00:00
{getFilterCount() > 0 && (
<span className="bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center font-semibold">
{getFilterCount()}
2025-10-24 15:01:01 +00:00
</span>
2025-10-23 14:39:13 +00:00
)}
</Button>
<Button
onClick={applyFilters}
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
>
Aplicar filtros
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
2025-10-22 22:20:44 +00:00
}