fix: filtro customizado

This commit is contained in:
Alessandro Gonçaalves 2025-10-22 18:55:32 -03:00
parent 9f7e19d80d
commit a7adda84a5
1 changed files with 422 additions and 189 deletions

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { DataGrid, GridToolbar } from "@mui/x-data-grid"; import { DataGrid, GridToolbar, GridColDef, GridFilterModel } from "@mui/x-data-grid";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@ -11,6 +11,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter, DialogFooter,
DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { import {
Select, Select,
@ -19,7 +20,8 @@ import {
SelectContent, SelectContent,
SelectItem, SelectItem,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Download, Filter, X } from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox";
import { Download, Filter, X, Search, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";
interface AnaliticoItem { interface AnaliticoItem {
@ -65,11 +67,207 @@ interface AnaliticoProps {
}; };
} }
// Componente de filtro customizado estilo Excel
interface ExcelFilterProps {
column: GridColDef;
data: any[];
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,
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);
// Obter valores únicos da coluna
const uniqueValues = React.useMemo(() => {
const values = data
.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;
}, [data, column.field]);
// Filtrar valores baseado na busca
const filteredValues = React.useMemo(() => {
if (!searchTerm) return uniqueValues;
return uniqueValues.filter((value) =>
value.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [uniqueValues, searchTerm]);
// Verificar se todos estão selecionados
React.useEffect(() => {
setSelectAll(selectedValues.length === filteredValues.length && filteredValues.length > 0);
}, [selectedValues, filteredValues]);
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedValues(filteredValues);
} else {
setSelectedValues([]);
}
};
const handleValueToggle = (value: string, checked: boolean) => {
if (checked) {
setSelectedValues([...selectedValues, value]);
} else {
setSelectedValues(selectedValues.filter((v) => v !== value));
}
};
const handleApply = () => {
onFilterChange(column.field, selectedValues);
setIsOpen(false);
};
const handleClear = () => {
setSelectedValues([]);
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"
className="h-8 w-8 p-0"
onClick={() => setIsOpen(true)}
>
<ArrowUpDown className="h-4 w-4" />
</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) { export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
const [data, setData] = React.useState<AnaliticoItem[]>([]); const [data, setData] = React.useState<AnaliticoItem[]>([]);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [globalFilter, setGlobalFilter] = React.useState(""); const [globalFilter, setGlobalFilter] = React.useState("");
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [columnFilters, setColumnFilters] = React.useState<Record<string, string[]>>({});
const [columnSorts, setColumnSorts] = React.useState<Record<string, 'asc' | 'desc' | null>>({});
const [conditions, setConditions] = React.useState([ const [conditions, setConditions] = React.useState([
{ column: "", operator: "contains", value: "" }, { column: "", operator: "contains", value: "" },
]); ]);
@ -77,6 +275,21 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
// Estado para armazenar filtros externos (vindos do teste.tsx) // Estado para armazenar filtros externos (vindos do teste.tsx)
const [filtrosExternos, setFiltrosExternos] = React.useState(filtros); const [filtrosExternos, setFiltrosExternos] = React.useState(filtros);
// 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
}));
}, []);
// Atualizar filtros externos quando os props mudarem // Atualizar filtros externos quando os props mudarem
React.useEffect(() => { React.useEffect(() => {
console.log('🔄 Analítico - useEffect dos filtros chamado'); console.log('🔄 Analítico - useEffect dos filtros chamado');
@ -145,199 +358,219 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
}, [fetchData]); }, [fetchData]);
// Definir colunas do DataGridPro // Definir colunas do DataGridPro
const columns = React.useMemo(() => [ const columns = React.useMemo(() => {
{ const baseColumns = [
field: "data_vencimento", {
headerName: "Dt Venc", field: "data_vencimento",
width: 150, headerName: "Dt Venc",
sortable: true, width: 150,
resizable: true, sortable: true,
renderCell: (params: any) => { resizable: true,
if (!params.value) return "-"; renderCell: (params: any) => {
try { if (!params.value) return "-";
return new Date(params.value).toLocaleDateString("pt-BR"); try {
} catch (error) { return new Date(params.value).toLocaleDateString("pt-BR");
return params.value; } catch (error) {
} return params.value;
}
},
}, },
}, {
{ field: "data_caixa",
field: "data_caixa", headerName: "Dt Caixa",
headerName: "Dt Caixa", width: 130,
width: 130, sortable: true,
sortable: true, resizable: true,
resizable: true, renderCell: (params: any) => {
renderCell: (params: any) => { if (!params.value) return "-";
if (!params.value) return "-"; try {
try { return new Date(params.value).toLocaleDateString("pt-BR");
return new Date(params.value).toLocaleDateString("pt-BR"); } catch (error) {
} catch (error) { return params.value;
return params.value; }
} },
}, },
}, {
{ field: "entidade",
field: "entidade", headerName: "Entidade",
headerName: "Entidade", width: 100,
width: 100, sortable: true,
sortable: true, resizable: true,
resizable: true, renderCell: (params: any) => params.value || "-",
renderCell: (params: any) => params.value || "-",
},
{
field: "codigo_fornecedor",
headerName: "Cod.Fornec",
width: 140,
sortable: true,
resizable: true,
},
{
field: "nome_fornecedor",
headerName: "Fornecedor",
flex: 1,
minWidth: 200,
sortable: true,
resizable: true,
},
{
field: "codigo_centrocusto",
headerName: "C Custo",
width: 130,
sortable: true,
resizable: true,
},
{
field: "codigo_conta",
headerName: "Cod.Conta",
width: 150,
sortable: true,
resizable: true,
},
{
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) => {
const value = params.value;
if (value === null || value === undefined || value === "") 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={numValue < 0 ? "text-red-600" : "text-gray-900"}>
{formatted}
</span>
);
}, },
}, {
{ field: "codigo_fornecedor",
field: "valor_previsto", headerName: "Cod.Fornec",
headerName: "Vl.Previsto", width: 140,
type: "number" as const, sortable: true,
width: 130, resizable: true,
sortable: true,
resizable: true,
renderCell: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "" || 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={numValue < 0 ? "text-red-600" : "text-gray-900"}>
{formatted}
</span>
);
}, },
}, {
{ field: "nome_fornecedor",
field: "valor_confirmado", headerName: "Fornecedor",
headerName: "Vl.Confirmado", flex: 1,
type: "number" as const, minWidth: 200,
width: 140, sortable: true,
sortable: true, resizable: true,
resizable: true,
renderCell: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "" || 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={numValue < 0 ? "text-red-600" : "text-gray-900"}>
{formatted}
</span>
);
}, },
}, {
{ field: "codigo_centrocusto",
field: "valor_pago", headerName: "C Custo",
headerName: "Vl.Pago", width: 130,
type: "number" as const, sortable: true,
width: 130, resizable: true,
sortable: true,
resizable: true,
renderCell: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "" || 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={numValue < 0 ? "text-red-600" : "text-gray-900"}>
{formatted}
</span>
);
}, },
}, {
{ field: "codigo_conta",
field: "historico", headerName: "Cod.Conta",
headerName: "Historico", width: 150,
flex: 1, sortable: true,
minWidth: 250, resizable: true,
sortable: true, },
resizable: true, {
}, field: "conta",
{ headerName: "Conta",
field: "historico2", flex: 1,
headerName: "Historico 2", minWidth: 180,
flex: 1, sortable: true,
minWidth: 300, resizable: true,
sortable: true, },
resizable: true, {
}, field: "valor",
{ headerName: "Vl.Realizado",
field: "numero_lancamento", type: "number" as const,
headerName: "Num.Lanc", width: 140,
width: 80, sortable: true,
sortable: true, resizable: true,
resizable: true, renderCell: (params: any) => {
renderCell: (params: any) => params.value || "-", const value = params.value;
}, if (value === null || value === undefined || value === "") return "-";
] as any, []); 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={numValue < 0 ? "text-red-600" : "text-gray-900"}>
{formatted}
</span>
);
},
},
{
field: "valor_previsto",
headerName: "Vl.Previsto",
type: "number" as const,
width: 130,
sortable: true,
resizable: true,
renderCell: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "" || 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={numValue < 0 ? "text-red-600" : "text-gray-900"}>
{formatted}
</span>
);
},
},
{
field: "valor_confirmado",
headerName: "Vl.Confirmado",
type: "number" as const,
width: 140,
sortable: true,
resizable: true,
renderCell: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "" || 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={numValue < 0 ? "text-red-600" : "text-gray-900"}>
{formatted}
</span>
);
},
},
{
field: "valor_pago",
headerName: "Vl.Pago",
type: "number" as const,
width: 130,
sortable: true,
resizable: true,
renderCell: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "" || 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={numValue < 0 ? "text-red-600" : "text-gray-900"}>
{formatted}
</span>
);
},
},
{
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: 80,
sortable: true,
resizable: true,
renderCell: (params: any) => params.value || "-",
},
];
// Adicionar renderHeader com filtro Excel para todas as colunas
return baseColumns.map((col) => ({
...col,
renderHeader: (params: any) => (
<div className="flex items-center justify-between w-full">
<span className="text-sm font-medium">{col.headerName}</span>
<ExcelFilter
column={col}
data={data}
onFilterChange={handleColumnFilterChange}
onSortChange={handleColumnSortChange}
currentFilter={columnFilters[col.field] || []}
currentSort={columnSorts[col.field] || null}
/>
</div>
),
}));
}, [data, columnFilters, columnSorts, handleColumnFilterChange, handleColumnSortChange]);
// Calcular totais das colunas de valores // Calcular totais das colunas de valores
const columnTotals = React.useMemo(() => { const columnTotals = React.useMemo(() => {