624 lines
24 KiB
TypeScript
624 lines
24 KiB
TypeScript
'use client';
|
|
|
|
import * as React from "react";
|
|
import {
|
|
useReactTable,
|
|
getCoreRowModel,
|
|
getSortedRowModel,
|
|
getFilteredRowModel,
|
|
flexRender,
|
|
} from "@tanstack/react-table";
|
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
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";
|
|
import { ChevronUp, ChevronDown, Download } from "lucide-react";
|
|
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;
|
|
valor: number;
|
|
historico: string;
|
|
historico2: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
interface AnaliticoProps {
|
|
filtros: {
|
|
dataInicio: string;
|
|
dataFim: string;
|
|
centroCusto?: string;
|
|
codigoGrupo?: string;
|
|
codigoSubgrupo?: string;
|
|
codigoConta?: string;
|
|
};
|
|
}
|
|
|
|
export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
|
const [data, setData] = React.useState<AnaliticoItem[]>([]);
|
|
const [loading, setLoading] = React.useState(false);
|
|
const [globalFilter, setGlobalFilter] = React.useState("");
|
|
const [columnFilters, setColumnFilters] = React.useState<any[]>([]);
|
|
const [open, setOpen] = React.useState(false);
|
|
const [conditions, setConditions] = React.useState([{ column: "", operator: "contains", value: "" }]);
|
|
const [isScrolled, setIsScrolled] = React.useState(false);
|
|
|
|
const fetchData = React.useCallback(async () => {
|
|
// Só faz a requisição se tiver dataInicio e dataFim
|
|
if (!filtros.dataInicio || !filtros.dataFim) {
|
|
setData([]);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const params = new URLSearchParams({
|
|
dataInicio: filtros.dataInicio,
|
|
dataFim: filtros.dataFim,
|
|
...(filtros.centroCusto && { centroCusto: filtros.centroCusto }),
|
|
...(filtros.codigoGrupo && { codigoGrupo: filtros.codigoGrupo }),
|
|
...(filtros.codigoSubgrupo && {
|
|
codigoSubgrupo: filtros.codigoSubgrupo,
|
|
}),
|
|
...(filtros.codigoConta && { codigoConta: filtros.codigoConta }),
|
|
});
|
|
|
|
const response = await fetch(`/api/analitico?${params}`);
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
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);
|
|
}
|
|
}, [filtros]);
|
|
|
|
React.useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
const columns = React.useMemo(
|
|
() => [
|
|
{
|
|
accessorKey: "data_competencia",
|
|
header: "Data Comp.",
|
|
filterFn: "advancedText",
|
|
cell: ({ getValue }: { getValue: () => string }) => {
|
|
const value = getValue();
|
|
return new Date(value).toLocaleDateString('pt-BR');
|
|
}
|
|
},
|
|
{
|
|
accessorKey: "data_vencimento",
|
|
header: "Data Venc.",
|
|
filterFn: "advancedText",
|
|
cell: ({ getValue }: { getValue: () => string }) => {
|
|
const value = getValue();
|
|
return new Date(value).toLocaleDateString('pt-BR');
|
|
}
|
|
},
|
|
{
|
|
accessorKey: "data_caixa",
|
|
header: "Data Caixa",
|
|
filterFn: "advancedText",
|
|
cell: ({ getValue }: { getValue: () => string }) => {
|
|
const value = getValue();
|
|
return new Date(value).toLocaleDateString('pt-BR');
|
|
}
|
|
},
|
|
{ accessorKey: "codigo_fornecedor", header: "Cód. Fornec.", filterFn: "advancedText" },
|
|
{ accessorKey: "nome_fornecedor", header: "Fornecedor", filterFn: "advancedText" },
|
|
{ accessorKey: "codigo_centrocusto", header: "Cód. Centro", filterFn: "advancedText" },
|
|
{ accessorKey: "codigo_conta", header: "Cód. Conta", filterFn: "advancedText" },
|
|
{ accessorKey: "conta", header: "Conta", filterFn: "advancedText" },
|
|
{
|
|
accessorKey: "valor",
|
|
header: "Valor",
|
|
filterFn: "advancedText",
|
|
cell: ({ getValue }: { getValue: () => number }) => {
|
|
const value = getValue();
|
|
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>
|
|
);
|
|
}
|
|
},
|
|
{ accessorKey: "historico", header: "Histórico", filterFn: "advancedText" },
|
|
{ accessorKey: "historico2", header: "Histórico 2", filterFn: "advancedText" },
|
|
{ accessorKey: "recnum", header: "Recnum", filterFn: "advancedText" },
|
|
],
|
|
[]
|
|
);
|
|
|
|
const filterFns = React.useMemo(
|
|
() => ({
|
|
advancedText: (row: any, columnId: string, filter: any) => {
|
|
if (!filter) return true;
|
|
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();
|
|
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;
|
|
}
|
|
},
|
|
}),
|
|
[]
|
|
);
|
|
|
|
const table = useReactTable({
|
|
data,
|
|
columns,
|
|
state: { globalFilter, columnFilters },
|
|
onGlobalFilterChange: setGlobalFilter,
|
|
onColumnFiltersChange: setColumnFilters,
|
|
filterFns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
});
|
|
|
|
const parentRef = React.useRef<HTMLDivElement>(null);
|
|
const rowVirtualizer = useVirtualizer({
|
|
count: table.getRowModel().rows.length,
|
|
getScrollElement: () => parentRef.current,
|
|
estimateSize: () => 36,
|
|
overscan: 20,
|
|
});
|
|
|
|
const virtualRows = rowVirtualizer.getVirtualItems();
|
|
|
|
React.useEffect(() => {
|
|
const handleScroll = () => {
|
|
if (!parentRef.current) return;
|
|
setIsScrolled(parentRef.current.scrollTop > 0);
|
|
};
|
|
const el = parentRef.current;
|
|
el?.addEventListener("scroll", handleScroll);
|
|
return () => el?.removeEventListener("scroll", handleScroll);
|
|
}, []);
|
|
|
|
const applyFilters = () => {
|
|
const validConditions = conditions.filter((c) =>
|
|
c.column && (c.operator === "empty" || c.operator === "notEmpty" || (c.value ?? "") !== "")
|
|
);
|
|
|
|
const filters = validConditions.map((c) => ({
|
|
id: c.column,
|
|
value: { operator: c.operator, value: c.value }
|
|
}));
|
|
|
|
setColumnFilters(filters);
|
|
setOpen(false);
|
|
};
|
|
|
|
const clearFilters = () => {
|
|
setConditions([{ column: "", operator: "contains", value: "" }]);
|
|
setColumnFilters([]);
|
|
};
|
|
|
|
const totalValor = data.reduce((sum, item) => {
|
|
const valor =
|
|
typeof item.valor === 'string' ? parseFloat(item.valor) : item.valor;
|
|
return sum + (isNaN(valor) ? 0 : valor);
|
|
}, 0);
|
|
|
|
const exportToExcel = () => {
|
|
if (data.length === 0) return;
|
|
|
|
// Preparar dados para exportação
|
|
const exportData = data.map((item) => ({
|
|
'Data Competência': new Date(item.data_competencia).toLocaleDateString(
|
|
'pt-BR'
|
|
),
|
|
'Data Vencimento': new Date(item.data_vencimento).toLocaleDateString(
|
|
'pt-BR'
|
|
),
|
|
'Data Caixa': new Date(item.data_caixa).toLocaleDateString('pt-BR'),
|
|
'Código Fornecedor': item.codigo_fornecedor,
|
|
Fornecedor: item.nome_fornecedor,
|
|
'Código Centro Custo': item.codigo_centrocusto,
|
|
'Centro Custo': item.codigo_centrocusto, // Assumindo que é o mesmo valor
|
|
'Código Conta': item.codigo_conta,
|
|
Conta: item.conta,
|
|
Valor:
|
|
typeof item.valor === 'string' ? parseFloat(item.valor) : item.valor,
|
|
Histórico: item.historico,
|
|
'Histórico 2': item.historico2,
|
|
Recnum: item.recnum,
|
|
}));
|
|
|
|
// Criar workbook
|
|
const wb = XLSX.utils.book_new();
|
|
const ws = XLSX.utils.json_to_sheet(exportData);
|
|
|
|
// Adicionar resumo na segunda aba
|
|
const resumoData = [
|
|
{ Métrica: 'Total de Registros', Valor: data.length },
|
|
{ Métrica: 'Valor Total', Valor: totalValor },
|
|
];
|
|
const wsResumo = XLSX.utils.json_to_sheet(resumoData);
|
|
|
|
// Adicionar abas ao workbook
|
|
XLSX.utils.book_append_sheet(wb, ws, 'Dados Analíticos');
|
|
XLSX.utils.book_append_sheet(wb, wsResumo, 'Resumo');
|
|
|
|
// Gerar nome do arquivo com data e hora
|
|
const now = new Date();
|
|
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-');
|
|
const fileName = `analitico_${timestamp}.xlsx`;
|
|
|
|
// Fazer download
|
|
XLSX.writeFile(wb, fileName);
|
|
};
|
|
|
|
return (
|
|
<Card className="w-full h-[85vh] shadow-xl rounded-2xl border-0 bg-gradient-to-br from-white to-gray-50/30">
|
|
<CardContent className="p-6 h-full flex flex-col">
|
|
<div className="flex justify-between mb-6 flex-wrap gap-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
|
|
<svg className="w-6 h-6 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" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-900">Análise Analítica</h2>
|
|
<p className="text-sm text-gray-500">Relatório detalhado de transações</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<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"
|
|
>
|
|
Filtros Avançados
|
|
</Button>
|
|
{data.length > 0 && (
|
|
<Button
|
|
onClick={exportToExcel}
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-green-50 hover:border-green-300 text-gray-700"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
Exportar XLSX
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative flex-1 bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
<table className="min-w-full border-collapse table-fixed">
|
|
<thead
|
|
className={`bg-gradient-to-r from-blue-50 to-indigo-50 sticky top-0 z-20 transition-all duration-200 ${isScrolled ? "shadow-lg" : "shadow-sm"}`}
|
|
>
|
|
{table.getHeaderGroups().map((hg) => (
|
|
<tr key={hg.id}>
|
|
{hg.headers.map((header) => {
|
|
const sorted = header.column.getIsSorted();
|
|
return (
|
|
<th
|
|
key={header.id}
|
|
onClick={header.column.getToggleSortingHandler()}
|
|
className="text-left px-4 py-4 border-b border-gray-200 cursor-pointer select-none group hover:bg-blue-100/50 transition-colors duration-150 whitespace-nowrap"
|
|
>
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span className="font-semibold text-gray-800 text-sm uppercase tracking-wide truncate">
|
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
</span>
|
|
<div className="flex flex-col flex-shrink-0">
|
|
{sorted === "asc" ? (
|
|
<ChevronUp className="w-4 h-4 text-blue-600" />
|
|
) : sorted === "desc" ? (
|
|
<ChevronDown className="w-4 h-4 text-blue-600" />
|
|
) : (
|
|
<div className="flex flex-col opacity-30 group-hover:opacity-60 transition-opacity">
|
|
<ChevronUp className="w-3 h-3 -mb-1" />
|
|
<ChevronDown className="w-3 h-3" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</th>
|
|
);
|
|
})}
|
|
</tr>
|
|
))}
|
|
</thead>
|
|
</table>
|
|
|
|
<div ref={parentRef} className="overflow-auto h-full bg-white">
|
|
<table className="min-w-full border-collapse table-fixed">
|
|
<tbody
|
|
style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: "relative" }}
|
|
>
|
|
{loading ? (
|
|
<tr>
|
|
<td colSpan={columns.length} className="p-12 text-center">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
<span className="text-gray-500 font-medium">Carregando dados analíticos...</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
) : virtualRows.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={columns.length} className="p-12 text-center">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center">
|
|
<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>
|
|
</div>
|
|
<span className="text-gray-500 font-medium">Nenhum dado analítico encontrado para os filtros aplicados.</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
virtualRows.map((virtualRow) => {
|
|
const row = table.getRowModel().rows[virtualRow.index];
|
|
return (
|
|
<tr
|
|
key={row.id}
|
|
className="hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30 transition-all duration-150 border-b border-gray-100"
|
|
style={{
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
transform: `translateY(${virtualRow.start}px)`,
|
|
display: "table",
|
|
width: "100%",
|
|
tableLayout: "fixed",
|
|
}}
|
|
>
|
|
{row.getVisibleCells().map((cell, cellIndex) => (
|
|
<td
|
|
key={cell.id}
|
|
className={`px-4 py-3 text-sm whitespace-nowrap overflow-hidden ${
|
|
cellIndex === 0 ? 'font-medium text-gray-900' : 'text-gray-700'
|
|
} ${
|
|
cell.column.id === 'valor' ? 'text-right font-semibold' : ''
|
|
}`}
|
|
title={String(cell.getValue())}
|
|
>
|
|
<div className="truncate">
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</div>
|
|
</td>
|
|
))}
|
|
</tr>
|
|
);
|
|
})
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{data.length > 0 && (
|
|
<div className="mt-6 p-6 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-xl shadow-sm">
|
|
<div className="flex justify-between items-center">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
|
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
</svg>
|
|
</div>
|
|
<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>
|
|
)}
|
|
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogContent className="max-w-2xl w-full mx-4 bg-white">
|
|
<DialogHeader className="pb-4">
|
|
<DialogTitle className="text-xl font-semibold text-gray-900">Filtros Avançados</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 max-h-96 overflow-y-auto bg-white">
|
|
{conditions.map((cond, idx) => (
|
|
<div key={idx} className="flex gap-3 items-start p-4 bg-gray-50 rounded-lg border border-gray-200">
|
|
<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) => (
|
|
<SelectItem key={col.accessorKey} value={col.accessorKey}>
|
|
{col.header}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</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;
|
|
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>
|
|
|
|
<DialogFooter className="flex gap-3 pt-6 border-t border-gray-200">
|
|
<Button
|
|
variant="outline"
|
|
onClick={clearFilters}
|
|
className="flex-1 border-gray-300 text-gray-700 hover:bg-gray-50"
|
|
>
|
|
Limpar todos
|
|
</Button>
|
|
<Button
|
|
onClick={applyFilters}
|
|
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
|
|
>
|
|
Aplicar filtros
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
} |